@volter/supercode-teams 0.2.172 → 0.2.173

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/teams.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  // title set <pane> <text> | title ls
20
20
  // attention ls | rank | request <pane> [--urgency now|soon|whenever] [--reason text] | clear <pane>
21
21
  // watch [--interval ms] [--for ms]
22
- // launches get <key> | list operators: keyed node launch receipts
22
+ // launches get <key> | cancel <key> | list operators: keyed receipts; cancel only unissued keys
23
23
  // rpc <harness.v1 method> [json-params] operators: one harness.v1 request
24
24
  // log verify
25
25
  //
@@ -323,8 +323,9 @@ async function main() {
323
323
  return withNode(async (rpc) => out(await rpc.call('jobs.list', {})));
324
324
  case 'launches': {
325
325
  if (sub === 'get' && rest[0]) return withNode(async (rpc) => out(await rpc.call('launches.get', { key: rest[0] })));
326
+ if (sub === 'cancel' && rest[0]) return withNode(async (rpc) => out(await rpc.call('launches.cancel', { key: rest[0] })));
326
327
  if (sub === 'list') return withNode(async (rpc) => out(await rpc.call('launches.list', {})));
327
- die('launches get <key> | launches list [--machine NAME]');
328
+ die('launches get <key> | cancel <key> | list [--machine NAME]');
328
329
  return;
329
330
  }
330
331
  case 'rpc': {
package/machine.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  // request ids rewritten per connection, notifications routed
8
8
  // by the subscription/connection they name (never fanned out)
9
9
  // stream "terminal" a pane attachment under observe|control
10
- // stream "node" describe, launch, launches.get|list, panes.*, files.*,
10
+ // stream "node" describe, launch, launches.get|cancel|list, panes.*, files.*,
11
11
  // ports.*, attention.*, title.*
12
12
  // stream "frontend" frontend.v2 to a hosted runtime, one client id per caller
13
13
  // stream "port" one TCP connection to an announced port
@@ -1345,6 +1345,19 @@ export class TeamsMachine extends MachineCore {
1345
1345
  });
1346
1346
  }
1347
1347
 
1348
+ async #launchReceipt(found) {
1349
+ const session = found.pane ? await this.#paneByContext(found.pane) : null;
1350
+ return {
1351
+ key: found.key, pane: found.pane ?? null, status: found.status, by: found.by ?? null,
1352
+ cancelled: found.status === 'cancelled', outcome: found.outcome ?? null,
1353
+ cancelled_at: found.cancelled_at ?? null,
1354
+ live: Boolean(session && !session.dead), label: session?.label ?? null, activeCommand: session?.activeCommand ?? null,
1355
+ exit_output: found.pane && (!session || session.dead) ? paneExitOutput(found.pane) : null,
1356
+ exit: session?.dead ? deadPaneExit(session) : found.pane && !session ? paneExit(found.pane) : null,
1357
+ ...initialInputFacts(found),
1358
+ };
1359
+ }
1360
+
1348
1361
  async #dispatch(entry, method, params) {
1349
1362
  // A server-forwarded caller is re-admitted on every door: a grant revoked or expired
1350
1363
  // since the stream opened stops mattering NOW, not at the next stream.
@@ -1439,16 +1452,24 @@ export class TeamsMachine extends MachineCore {
1439
1452
  const programName = String(params.program).split(/[\\/]/).pop().replace(/\.(?:exe|cmd)$/i, '');
1440
1453
  if (programName === 'codex') params = { ...params, kind: 'codex' };
1441
1454
  const key = params.key ?? `lk_${randomBytes(9).toString('base64url')}`;
1442
- const existing = this.launches.find(key);
1443
- // A key whose launch is in flight, or whose pane still lives, is reused; a failed
1444
- // launch, or one whose pane has ended, may be launched again.
1445
- if (existing && (existing.status === 'issued' || (existing.status === 'ready' && await this.#paneByContext(existing.pane)))) {
1446
- return { key, pane: existing.pane, reused: true, status: existing.status };
1455
+ const by = await this.#callerOf(entry, params).catch(() => null);
1456
+ // No await between the final journal check and issuance: cancellation and issuance
1457
+ // serialize in this daemon even when caller resolution or pane reads yielded.
1458
+ let existing = this.launches.find(key);
1459
+ while (existing) {
1460
+ if (existing.status === 'cancelled') throw Object.assign(new Error(`launch key ${key} was cancelled before issuance; use a new key`), { code: 409, reason: 'launch_key_cancelled' });
1461
+ if (existing.status === 'issued') return { key, pane: existing.pane, reused: true, status: existing.status };
1462
+ if (existing.status === 'ready') {
1463
+ const session = await this.#paneByContext(existing.pane);
1464
+ const latest = this.launches.find(key);
1465
+ if (JSON.stringify(latest) !== JSON.stringify(existing)) { existing = latest; continue; }
1466
+ if (session && !session.dead) return { key, pane: existing.pane, reused: true, status: existing.status };
1467
+ }
1468
+ break;
1447
1469
  }
1448
1470
  const contextKey = params.pane ?? `p_${randomBytes(9).toString('base64url')}`;
1449
1471
  // Who started the pane: the launching session's (or board's) address, null for a person. A session's later
1450
1472
  // input on the pane is refused unless it is this one (#refuseForeign); close is not creator-restricted.
1451
- const by = await this.#callerOf(entry, params).catch(() => null);
1452
1473
  // Who to tell when the pane ends (`open --new … --notify-when-ended`): a session's mail address, and this
1453
1474
  // machine's name as the address a notice from its pane comes from.
1454
1475
  if (params.notify !== undefined && !(params.notify && typeof params.notify.to === 'string' && /^sc:[^:\s]+:[^:\s]+:\S+$/.test(params.notify.to) && typeof params.notify.machine === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(params.notify.machine))) throw Object.assign(new Error('launch notify is { to: <sc: address>, machine: <this machine\'s mail name> }'), { code: 400 });
@@ -1523,13 +1544,21 @@ export class TeamsMachine extends MachineCore {
1523
1544
  if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
1524
1545
  const found = this.launches.find(params.key);
1525
1546
  if (!found) return null;
1526
- const session = await this.#paneByContext(found.pane);
1527
- return {
1528
- exit_output: !session || session.dead ? paneExitOutput(found.pane) : null,
1529
- exit: session?.dead ? deadPaneExit(session) : !session ? paneExit(found.pane) : null,
1530
- key: found.key, pane: found.pane, live: Boolean(session && !session.dead), label: session?.label ?? null, activeCommand: session?.activeCommand ?? null,
1531
- ...initialInputFacts(found),
1532
- };
1547
+ return this.#launchReceipt(found);
1548
+ }
1549
+ case 'launches.cancel': {
1550
+ if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
1551
+ if (typeof params.key !== 'string' || !params.key.trim()) throw Object.assign(new Error('launch cancellation requires a nonempty key'), { code: 400 });
1552
+ const by = await this.#callerOf(entry, params).catch(() => null);
1553
+ // Retire only an absent key. An issued, live or failed launch keeps its actual
1554
+ // receipt; cancellation never closes a pane or guesses that issuance did not happen.
1555
+ let found = this.launches.find(params.key);
1556
+ if (!found) {
1557
+ this.launches.record({ key: params.key, pane: null, by, status: 'cancelled', outcome: 'not_issued', cancelled_at: Date.now() });
1558
+ found = this.launches.find(params.key);
1559
+ act('launches.cancel', params.key, 'ok', 'not issued; key retired');
1560
+ } else act('launches.cancel', params.key, found.status === 'cancelled' ? 'ok' : 'refused', found.status);
1561
+ return this.#launchReceipt(found);
1533
1562
  }
1534
1563
  case 'launches.list': {
1535
1564
  // Everything this daemon launched, by key, with whether the pane still lives: how an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.172",
3
+ "version": "0.2.173",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",