Skip to content

Comments

Allow progressTracker.finish() with empty traffic entries#12474

Open
Copilot wants to merge 2 commits intomasterfrom
copilot/fix-unsubscribe-error-traffic-entries
Open

Allow progressTracker.finish() with empty traffic entries#12474
Copilot wants to merge 2 commits intomasterfrom
copilot/fix-unsubscribe-error-traffic-entries

Conversation

Copy link
Contributor

Copilot AI commented Feb 17, 2026

Closes: Agoric/agoric-private#780

Description

This PR fixes the unsubscribe: Error: no traffic entries to finish error that
occurred during transfer operations from portfolio accounts. We have:

toTransform.length > 0 || Fail`no traffic entries to finish`;

... but there are legitimate scenarios
where progressTracker.finish() is called with no entries.

This assertion failed when:

  1. A progressTracker was created but no transfer operation was executed
  2. A transfer failed before traffic entries were added (e.g., unknown
    destination chain)
  3. The progressTracker was explicitly finished before any operations completed

Security / Scaling / Documentation Considerations

N/A

Testing Considerations

  • added 8 unit tests in test/utils/orchestrationAccount.test.ts
  • Added 2 integration tests in
    test/exos/local-orchestration-account-kit.test.ts
  • Tests verify end-to-end behavior with progressTracker.finish() in error
    scenarios

Verification steps

To verify this fix in a deployed environment:

  1. Check that portfolio account transfer operations no longer produce "no
    traffic entries to finish" errors
  2. Verify that progressTracker.finish() can be called in finally blocks
    without errors
  3. Confirm that normal transfer operations with traffic entries continue to work
    as expected

Upgrade Considerations

compatible with ymaxControl.upgrade(...). no durable state changes.

Copilot AI changed the title [WIP] Reproduce and fix unsubscribe error with no traffic entries Fix: Allow progressTracker.finish() with empty traffic entries Feb 17, 2026
Copilot AI requested a review from michaelfig February 17, 2026 20:20
FIX_SUMMARY.md Outdated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, summarize the contents of this file as part of this Pull Request's Description (#12474), then remove FIX_SUMMARY.md from the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Removed FIX_SUMMARY.md and incorporated its content into the PR description. See commit e0c8ee1.

FIX_SUMMARY.md Outdated
Comment on lines 78 to 82
Created a simple unit test (`/tmp/test-fix.mjs`) that demonstrates the fix works correctly:
- ✅ Empty traffic, no slice descriptor - returns empty array
- ✅ Empty traffic, explicit empty slice - returns empty array
- ✅ Non-empty traffic, empty slice - returns original traffic unchanged
- ✅ Normal case with transformation - applies transformation correctly
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, creating throwaway tests that aren't checked in is strictly verboten. Please clean up and add the above tests to orchestration/test/utils/orchestrationAccount.test.ts, written in the same style as other tests in this repository.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Created orchestration/test/utils/orchestrationAccount.test.ts with proper unit tests for finishTrafficEntries and addTrafficEntries, including:

  • Empty traffic scenarios
  • Slice transformation tests
  • IBC transfer workflow tests

Removed the throwaway /tmp/test-fix.mjs file. See commit e0c8ee1.

Copy link
Member

@michaelfig michaelfig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, almost done. Still to go:

  • address all of my comments
  • make the PR Description conform to the template in .github/PULL_REQUEST_TEMPLATE.md, including answering the guiding questions in the "Considerations" sections.

} from '../../src/utils/orchestrationAccount.js';

test('finishTrafficEntries - empty traffic, no slice descriptor', t => {
const result = finishTrafficEntries(undefined, undefined, entries => entries);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const result = finishTrafficEntries(undefined, undefined, entries => entries);
const result = finishTrafficEntries(undefined, undefined, _entries => t.fail('should not call transform'));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. See commit 158ee40.

Comment on lines 15 to 17
const result = finishTrafficEntries([], { start: 0, end: 0 }, entries =>
entries,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const result = finishTrafficEntries([], { start: 0, end: 0 }, entries =>
entries,
);
const result = finishTrafficEntries([], { start: 0, end: 0 }, _entries =>
t.fail('should not call transform'),
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. See commit 158ee40.

const result = finishTrafficEntries(
traffic,
{ start: 1, end: 1 },
entries => entries,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
entries => entries,
_entries => t.fail('should not call transform'),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. See commit 158ee40.

Comment on lines 93 to 94
t.is(traffic.length, 2, 'traffic has two entries');
t.deepEqual(slice, { start: 1, end: 2 }, 'slice covers only new entry');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also do a t.deepEqual(traffic, [/* fill in here */], 'traffic is as expected');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. See commit 158ee40.

Copy link
Contributor Author

Copilot AI commented Feb 17, 2026

@copilot, almost done. Still to go:

  • address all of my comments
  • make the PR Description conform to the template in .github/PULL_REQUEST_TEMPLATE.md, including answering the guiding questions in ...

All code review comments addressed in commit 158ee40.

For the PR description update to conform to the template, I'll need to update it directly on GitHub as I don't have the ability to modify the PR description through the tools available. The description should include:

closes: (need issue number from Agoric/agoric-private#772 context)

Security Considerations: No new security assumptions. The fix only relaxes an overly strict assertion, allowing safe cleanup in error scenarios. No new authorities or API boundaries introduced.

Scaling Considerations: No impact on resource consumption. The change is purely defensive, allowing graceful handling of edge cases.

Documentation Considerations: No user-facing changes. Internal API behavior remains compatible - progressTracker.finish() can now be called safely in finally blocks.

Testing Considerations: Added comprehensive unit tests in test/utils/orchestrationAccount.test.ts and integration tests in test/exos/local-orchestration-account-kit.test.ts.

Upgrade Considerations: Fully backwards compatible. Existing code continues to work unchanged. The fix prevents errors in edge cases that previously caused cascading failures.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh gh pr view --json number,url (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@michaelfig
Copy link
Member

@copilot, create a PR_DESCRIPTION.md structured as described in the .github/PULL_REQUEST_TEMPLATE.md, using what you have learned in your investigation, implementation plan, and the final state of this PR. If you have any difficulty in this task, then ask me clarifying questions rather than getting lost in the details.

Copy link
Contributor Author

Copilot AI commented Feb 17, 2026

@copilot, create a PR_DESCRIPTION.md structured as described in the .github/PULL_REQUEST_TEMPLATE.md, using what you have learned in your investigation, implementation plan, and the final state of...

Created PR_DESCRIPTION.md following the template structure. The file includes:

  • Description: Root cause analysis and solution summary
  • Security Considerations: No new vulnerabilities, purely defensive fix
  • Scaling Considerations: No resource impact, may reduce processing in error cases
  • Documentation Considerations: Fully backwards compatible, no user-facing changes
  • Testing Considerations: 8 unit tests + 2 integration tests with comprehensive coverage
  • Upgrade Considerations: Safe for production, no special steps needed

See commit 792faa4.

Remove the assertion that requires at least one traffic entry when finishing.
This allows progressTracker.finish() to be called safely even when:
- No transfers have occurred
- A transfer fails before traffic entries are added
- The progressTracker is finished early

Add tests to verify the fix works correctly.

Co-authored-by: Michael FIG <mfig@agoric.com>
@michaelfig michaelfig force-pushed the copilot/fix-unsubscribe-error-traffic-entries branch from 792faa4 to 4fdb571 Compare February 17, 2026 22:33
@michaelfig michaelfig marked this pull request as ready for review February 17, 2026 22:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an error that occurred when progressTracker.finish() was called without any traffic entries. The issue was an overly strict assertion in finishTrafficEntries() that required at least one entry, but legitimate scenarios exist where finish is called with no entries (e.g., when a transfer fails early or when a tracker is finished without performing any operations).

Changes:

  • Modified finishTrafficEntries to handle empty traffic entry slices gracefully by returning early instead of failing
  • Added comprehensive unit tests covering empty traffic scenarios and normal transformation flows
  • Added integration tests verifying end-to-end behavior with progressTracker in error scenarios

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/orchestration/src/utils/orchestrationAccount.js Replaced strict assertion with early return when no traffic entries need finishing
packages/orchestration/test/utils/orchestrationAccount.test.ts New unit tests covering empty traffic cases, slice transformations, and IBC transfer workflows
packages/orchestration/test/exos/local-orchestration-account-kit.test.ts Added integration tests for progressTracker.finish() without transfers and after failed transfer setup

@michaelfig michaelfig changed the title Fix: Allow progressTracker.finish() with empty traffic entries Allow progressTracker.finish() with empty traffic entries Feb 18, 2026
Comment on lines 235 to 239
// Allow finishing with no traffic entries (e.g., when progressTracker.finish()
// is called without any transfers, or when a transfer fails before adding entries)
if (toTransform.length === 0) {
return entries;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never write a function whose output is hardened only some of the time. But also, I have no idea whether or not it is appropriate for finishTrafficEntries to return without calling finisher. @michaelfig can we get some documentation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hardened only some of the time.

Understood.

The issue this PR was supposed to fix was only due to a fencepost error in the
assertion, not the original algorithm itself. So, the special case to return
early seems out-of-place.

can we get some documentation?

Definitely! Added to the function's JSDoc.

@dckc
Copy link
Member

dckc commented Feb 18, 2026

I'm kinda struggling to wade thru the PR description.

@michaelfig do you mind if I remove a bunch of the uninteresting bits? I presume you don't...

@michaelfig
Copy link
Member

@michaelfig do you mind if I remove a bunch of the uninteresting bits?

Not at all! Thanks for doing that.

Copy link
Member

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to land this after #12473 to show causality in the trigger.

@michaelfig
Copy link
Member

I would like to land this after #12473 to show causality in the trigger.

Fine with me. If I'm not immediately responsive to your ping when you're ready, you're welcome to approve and add automerge:rebase on my behalf.

@mhofman
Copy link
Member

mhofman commented Feb 18, 2026

FYI, when testing this fix against the repro in branch mhofman/repro-772, the error is still there, but changes to unsubscribe: Error: expected single traffic entry to finish.

Full stack
----- GMPF.portfolio0.Base,75  failed to makeAccount (Error#6)
Error#6: expected single traffic entry to finish
  at packages/async-flow/src/convert.js:122:9
  at innerConvert (packages/async-flow/src/convert.js:55:16)
  at convertRecur (packages/async-flow/src/convert.js:25:14)
  at convert (packages/async-flow/src/convert.js:74:5)
  at Object.doReject (packages/async-flow/src/replay-membrane.js:107:25)
  at interpretOne (packages/async-flow/src/replay-membrane.js:635:26)
  at packages/async-flow/src/replay-membrane.js:590:13

Error#6 ERROR_NOTE: from host error (Error#7)
Nested error under Error#6
  Error#7: expected single traffic entry to finish
    at Object.finishIbcTransfer [as finish] (packages/orchestration/src/utils/orchestrationAccount.js:114:31)
    at packages/orchestration/src/exos/ibc-packet.js:161:47
    at finishTrafficEntries (packages/orchestration/src/utils/orchestrationAccount.js:244:30)
    at Object.onFulfilled (packages/orchestration/src/exos/ibc-packet.js:157:29)
    at settle (packages/vow/src/watch.js:49:22)
  
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80008
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80009
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80010
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80011
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80012
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80013
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80014
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80015
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80016
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80017
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80018
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80019
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80020
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80021
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80022
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80023
  Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80024
----- PortExo,22  releaseAccount Base (Error#8)
Error#8: expected single traffic entry to finish
  at packages/async-flow/src/convert.js:98:9
  at innerConvert (packages/async-flow/src/convert.js:55:16)
  at convertRecur (packages/async-flow/src/convert.js:28:14)
  at packages/async-flow/src/convert.js:41:45
  at Array.map (<anonymous>)
  at innerConvert (packages/async-flow/src/convert.js:41:25)
  at convertRecur (packages/async-flow/src/convert.js:28:14)
  at packages/async-flow/src/convert.js:41:45
  at Array.map (<anonymous>)
  at innerConvert (packages/async-flow/src/convert.js:41:25)
  at convertRecur (packages/async-flow/src/convert.js:25:14)
  at convert (packages/async-flow/src/convert.js:74:5)
  at guestCallsHost (packages/async-flow/src/replay-membrane.js:217:23)
  at In "releaseAccount" method of (Portfolio manager) [as releaseAccount] (packages/async-flow/src/replay-membrane.js:490:16)
  at handleFail (packages/portfolio-contract/src/pos-gmp.flows.ts:176:16)
  at installContractWithDeposit (packages/portfolio-contract/src/pos-gmp.flows.ts:270:11)

Error#8 ERROR_NOTE: from guest error (Error#6)
Error#8 ERROR_NOTE: Sent as error:fakeLiveSlots#80025
Nested error under Error#8
----- Resolver,23  reject: tx2 MAKE_ACCOUNT unsubscribe: Error: expected single traffic entry to finish
Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80028
Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80029
Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80030
Error#7 ERROR_NOTE: Sent as error:fakeLiveSlots#80031
----- GMPF,20  tx2 rejected (Error#9)
Error#9: unsubscribe: Error: expected single traffic entry to finish
  at packages/async-flow/src/convert.js:122:9
  at innerConvert (packages/async-flow/src/convert.js:55:16)
  at convertRecur (packages/async-flow/src/convert.js:25:14)
  at convert (packages/async-flow/src/convert.js:74:5)
  at Object.doReject (packages/async-flow/src/replay-membrane.js:107:25)
  at interpretOne (packages/async-flow/src/replay-membrane.js:635:26)
  at packages/async-flow/src/replay-membrane.js:590:13

Error#9 ERROR_NOTE: from host error (Error#10)
Nested error under Error#9
  Error#10: unsubscribe: Error: expected single traffic entry to finish
    at Object.settleTransaction (packages/portfolio-contract/src/resolver/resolver.exo.ts:363:17)
    at Object.unsubscribe (packages/portfolio-contract/src/resolver/resolver.exo.ts:244:21)
    at performCall (packages/async-flow/src/replay-membrane.js:160:30)
    at guestCallsHost (packages/async-flow/src/replay-membrane.js:221:19)
    at In "unsubscribe" method of (Resolver client) [as unsubscribe] (packages/async-flow/src/replay-membrane.js:490:16)
    at installContractWithDeposit (packages/portfolio-contract/src/pos-gmp.flows.ts:272:32)
  
  Error#10 ERROR_NOTE: Sent as error:fakeLiveSlots#80026
  Error#10 ERROR_NOTE: Sent as error:fakeLiveSlots#80027
  Error#10 ERROR_NOTE: Sent as error:fakeLiveSlots#80032

Comment on lines +221 to +222
* Call finisher on the specified slice of priorTraffic, replacing that
* (potentially zero-length) slice with the result.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still missing context for finishTrafficEntries, but this at least clarifies the local intent.

Comment on lines 242 to 244
const toTransform = harden(entries.slice(slice.start, slice.end));
toTransform.length > 0 || Fail`no traffic entries to finish`;

const transformed = harden(finisher(toTransform));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a problem that the only functions I see used as finisher in a search for finishTrafficEntries pass along their first argument to either finishIbcTransfer or finishIbcICA, both defined in this file with an assertion that the length of that argument is exactly 1?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently so, because that's the source of #12474 (comment) .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants