@p4code/cli 0.6.2 → 0.6.4

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/dist/bin.mjs CHANGED
@@ -29,15 +29,15 @@ import * as Duration from "effect/Duration";
29
29
  import * as Option from "effect/Option";
30
30
  import * as SchemaTransformation from "effect/SchemaTransformation";
31
31
  import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema";
32
- import * as HttpApi from "effect/unstable/httpapi/HttpApi";
33
- import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
34
- import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
35
- import * as HttpApiMiddleware from "effect/unstable/httpapi/HttpApiMiddleware";
36
32
  import * as HttpServerRespondable$1 from "effect/unstable/http/HttpServerRespondable";
37
33
  import * as HttpServerResponse$1 from "effect/unstable/http/HttpServerResponse";
38
34
  import * as SchemaIssue from "effect/SchemaIssue";
39
35
  import * as SchemaGetter from "effect/SchemaGetter";
40
36
  import * as Struct from "effect/Struct";
37
+ import * as HttpApi from "effect/unstable/httpapi/HttpApi";
38
+ import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
39
+ import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
40
+ import * as HttpApiMiddleware from "effect/unstable/httpapi/HttpApiMiddleware";
41
41
  import { Schema } from "effect";
42
42
  import * as Rpc from "effect/unstable/rpc/Rpc";
43
43
  import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
@@ -263,10 +263,10 @@ const make$122 = () => {
263
263
  })
264
264
  };
265
265
  };
266
- const layer$122 = Layer.sync(NetService, make$122);
266
+ const layer$123 = Layer.sync(NetService, make$122);
267
267
  //#endregion
268
268
  //#region package.json
269
- var version$1 = "0.6.2";
269
+ var version$1 = "0.6.4";
270
270
  //#endregion
271
271
  //#region src/config.ts
272
272
  /**
@@ -288,7 +288,7 @@ var ServerConfig$1 = class extends Context.Service()("@p4code/cli/config/ServerC
288
288
  static layerTest = (cwd, baseDirOrPrefix) => layerTest$4(cwd, baseDirOrPrefix);
289
289
  };
290
290
  const make$121 = (config) => ServerConfig$1.of(config);
291
- const layer$121 = (config) => Layer.succeed(ServerConfig$1, make$121(config));
291
+ const layer$122 = (config) => Layer.succeed(ServerConfig$1, make$121(config));
292
292
  const deriveServerPaths = Effect.fn(function* (baseDir, devUrl, options = {}) {
293
293
  const { join } = yield* Path$1.Path;
294
294
  const stateDir = join(baseDir, devUrl !== void 0 && !options.baseDirIsExplicit ? "dev" : "userdata");
@@ -1377,276 +1377,6 @@ Schema$1.Struct({
1377
1377
  expiresAt: Schema$1.Number
1378
1378
  });
1379
1379
  //#endregion
1380
- //#region ../../packages/contracts/src/hubNotifications.ts
1381
- const MAX_NOTIFICATION_ID_CHARS = 128;
1382
- const MAX_PUSH_TOKEN_CHARS = 4096;
1383
- const MAX_HUB_ORIGIN_CHARS = 2048;
1384
- const MAX_ACTIVITY_TITLE_CHARS = 160;
1385
- const MAX_ACTIVITY_DETAIL_CHARS = 240;
1386
- const identifier = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_NOTIFICATION_ID_CHARS), Schema$1.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$(?![\s\S])/));
1387
- const pushToken = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PUSH_TOKEN_CHARS), Schema$1.isPattern(/^[\x21-\x7e]+$(?![\s\S])/));
1388
- /** Server emission is bound to the authenticated environment, never a supplied account. */
1389
- const HubAgentActivityInput = Schema$1.Struct({
1390
- eventId: identifier,
1391
- threadId: ThreadId,
1392
- revision: Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(0)),
1393
- state: Schema$1.Literals([
1394
- "running",
1395
- "approval_required",
1396
- "question_required",
1397
- "completed",
1398
- "failed",
1399
- "stopped"
1400
- ]),
1401
- title: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_ACTIVITY_TITLE_CHARS)),
1402
- detail: Schema$1.String.check(Schema$1.isMaxLength(MAX_ACTIVITY_DETAIL_CHARS))
1403
- });
1404
- /** Each authenticated client owns its registration; tokens never belong in public client state. */
1405
- const HubPushRegistrationInput = Schema$1.Union([Schema$1.Struct({
1406
- platform: Schema$1.Literal("ios"),
1407
- token: pushToken,
1408
- sandbox: Schema$1.Boolean
1409
- }), Schema$1.Struct({
1410
- platform: Schema$1.Literal("android"),
1411
- token: pushToken
1412
- })]);
1413
- /** Explicit opt-in, independently reversible. Omission never implies permission. */
1414
- const HubNotificationPreferences = Schema$1.Struct({
1415
- attention: Schema$1.Boolean,
1416
- completion: Schema$1.Boolean,
1417
- liveActivity: Schema$1.Boolean
1418
- });
1419
- Schema$1.Struct({
1420
- registered: Schema$1.Boolean,
1421
- registrationGeneration: Schema$1.NullOr(identifier),
1422
- preferences: HubNotificationPreferences
1423
- });
1424
- /** ActivityKit update token, distinct from the app's APNs registration token. */
1425
- const HubActivityOwnership = Schema$1.Struct({
1426
- origin: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_HUB_ORIGIN_CHARS)),
1427
- environmentId: EnvironmentId,
1428
- clientId: HubClientId,
1429
- registrationGeneration: identifier,
1430
- threadId: ThreadId,
1431
- activityId: identifier
1432
- });
1433
- const HubLiveActivityRegistrationInput = Schema$1.Struct({
1434
- threadId: ThreadId,
1435
- registrationGeneration: identifier,
1436
- activityId: identifier,
1437
- token: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PUSH_TOKEN_CHARS), Schema$1.isPattern(/^(?:[a-fA-F0-9]{2})+$(?![\s\S])/)),
1438
- sandbox: Schema$1.Boolean,
1439
- p4Ownership: Schema$1.optional(HubActivityOwnership)
1440
- });
1441
- /** Close only this presentation; delayed requests must not retire a newer activity. */
1442
- const HubLiveActivityRemoveInput = Schema$1.Struct({
1443
- threadId: ThreadId,
1444
- registrationGeneration: identifier,
1445
- activityId: identifier
1446
- });
1447
- const HUB_TUNNEL_WEBSOCKET_PROTOCOL = "p4-hub-tunnel-v1";
1448
- const HUB_TUNNEL_INITIAL_WINDOW_BYTES = 256 * 1024;
1449
- const HUB_TUNNEL_MAX_WINDOW_BYTES = 1024 * 1024;
1450
- const MAX_HEADERS = 64;
1451
- const MAX_HEADER_NAME_CHARS = 128;
1452
- const MAX_HEADER_VALUE_CHARS = 8192;
1453
- const MAX_PATH_CHARS = 8192;
1454
- const MAX_PROTOCOLS = 16;
1455
- const MAX_PROTOCOL_CHARS = 128;
1456
- const HeaderName = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_HEADER_NAME_CHARS), Schema$1.isPattern(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$(?![\s\S])/));
1457
- const HeaderValue = Schema$1.String.check(Schema$1.isMaxLength(MAX_HEADER_VALUE_CHARS), Schema$1.isPattern(/^[\t\x20-\x7e]*$(?![\s\S])/));
1458
- const Headers$2 = Schema$1.Array(Schema$1.Tuple([HeaderName, HeaderValue])).check(Schema$1.isMaxLength(MAX_HEADERS));
1459
- const Path = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PATH_CHARS), Schema$1.isPattern(/^\/(?!\/)[^\s\\#\x00-\x1f\x7f]*$(?![\s\S])/));
1460
- const Protocol = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PROTOCOL_CHARS), Schema$1.isPattern(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$(?![\s\S])/));
1461
- const FailureReason = Schema$1.Literals([
1462
- "cancelled",
1463
- "unavailable",
1464
- "protocol_error"
1465
- ]);
1466
- const CloseCode = Schema$1.Int.check(Schema$1.makeFilter((code) => code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006 || code >= 3e3 && code <= 4999));
1467
- /** No caller-supplied account, environment, origin, or connector ownership authority. */
1468
- const HubTunnelControl = Schema$1.Union([
1469
- Schema$1.Struct({
1470
- type: Schema$1.Literal("connector_ready"),
1471
- protocolVersion: Schema$1.Literal(1)
1472
- }),
1473
- Schema$1.Struct({
1474
- type: Schema$1.Literal("http_request_start"),
1475
- method: Schema$1.Literals([
1476
- "GET",
1477
- "HEAD",
1478
- "POST",
1479
- "PUT",
1480
- "PATCH",
1481
- "DELETE",
1482
- "OPTIONS"
1483
- ]),
1484
- path: Path,
1485
- headers: Headers$2
1486
- }),
1487
- Schema$1.Struct({ type: Schema$1.Literal("http_request_end") }),
1488
- Schema$1.Struct({
1489
- type: Schema$1.Literal("http_request_abort"),
1490
- reason: FailureReason
1491
- }),
1492
- Schema$1.Struct({
1493
- type: Schema$1.Literal("http_response_start"),
1494
- status: Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(200), Schema$1.isLessThanOrEqualTo(599)),
1495
- headers: Headers$2
1496
- }),
1497
- Schema$1.Struct({ type: Schema$1.Literal("http_response_end") }),
1498
- Schema$1.Struct({
1499
- type: Schema$1.Literal("http_response_abort"),
1500
- reason: FailureReason
1501
- }),
1502
- Schema$1.Struct({
1503
- type: Schema$1.Literal("websocket_open"),
1504
- path: Path,
1505
- headers: Headers$2,
1506
- protocols: Schema$1.Array(Protocol).check(Schema$1.isMaxLength(MAX_PROTOCOLS))
1507
- }),
1508
- Schema$1.Struct({
1509
- type: Schema$1.Literal("websocket_accept"),
1510
- protocol: Schema$1.optional(Protocol)
1511
- }),
1512
- Schema$1.Struct({
1513
- type: Schema$1.Literal("websocket_reject"),
1514
- status: Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(400), Schema$1.isLessThanOrEqualTo(599)),
1515
- reason: FailureReason
1516
- }),
1517
- Schema$1.Struct({
1518
- type: Schema$1.Literal("websocket_close"),
1519
- code: CloseCode,
1520
- reason: FailureReason
1521
- }),
1522
- Schema$1.Struct({
1523
- type: Schema$1.Literal("window_update"),
1524
- creditBytes: Schema$1.Int.check(Schema$1.isGreaterThan(0), Schema$1.isLessThanOrEqualTo(HUB_TUNNEL_MAX_WINDOW_BYTES))
1525
- })
1526
- ]);
1527
- //#endregion
1528
- //#region ../../packages/contracts/src/environment.ts
1529
- const ExecutionEnvironmentPlatformOs = Schema$1.Literals([
1530
- "darwin",
1531
- "linux",
1532
- "windows",
1533
- "unknown"
1534
- ]);
1535
- const ExecutionEnvironmentPlatformArch = Schema$1.Literals([
1536
- "arm64",
1537
- "x64",
1538
- "other"
1539
- ]);
1540
- const ExecutionEnvironmentPlatform = Schema$1.Struct({
1541
- os: ExecutionEnvironmentPlatformOs,
1542
- arch: ExecutionEnvironmentPlatformArch
1543
- });
1544
- /** How a server can replace itself with another version when asked over RPC:
1545
- "boot-service" rewrites the systemd user unit and restarts it; "respawn"
1546
- installs the target version and respawns the foreground process. */
1547
- const ServerSelfUpdateMethod = Schema$1.Literals(["boot-service", "respawn"]);
1548
- /** What update path a client should offer for a server: one of the RPC
1549
- self-update methods above, or "desktop-managed" when the backend's
1550
- version belongs to the P4Code desktop app supervising it — updating the
1551
- app on that machine is the only way to update the server. */
1552
- const ServerSelfUpdateCapability = Schema$1.Literals([
1553
- "boot-service",
1554
- "respawn",
1555
- "desktop-managed"
1556
- ]);
1557
- const ExecutionEnvironmentCapabilities = Schema$1.Struct({
1558
- computerUseControls: Schema$1.optionalKey(Schema$1.Boolean),
1559
- usagePriceOverrides: Schema$1.optionalKey(Schema$1.Boolean),
1560
- projectAutoPull: Schema$1.optionalKey(Schema$1.Boolean),
1561
- sharedProjectModelDefault: Schema$1.optionalKey(Schema$1.Boolean),
1562
- projectScriptDefaults: Schema$1.optionalKey(Schema$1.Boolean),
1563
- customModelDefinitions: Schema$1.optionalKey(Schema$1.Boolean),
1564
- threadLifecycleV2: Schema$1.optionalKey(Schema$1.Boolean),
1565
- repositoryIdentity: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
1566
- connectionProbe: Schema$1.optionalKey(Schema$1.Boolean),
1567
- /** Server understands thread.settle / thread.unsettle commands. Absent on
1568
- pre-settlement servers, so clients treat missing as unsupported and
1569
- never send the commands under version skew. */
1570
- threadSettlement: Schema$1.optionalKey(Schema$1.Boolean),
1571
- /** Server understands thread.snooze / thread.unsnooze commands. Same
1572
- version-skew contract as threadSettlement. */
1573
- threadSnooze: Schema$1.optionalKey(Schema$1.Boolean),
1574
- /** Server understands thread.pin / thread.unpin / thread.pin.reorder
1575
- commands. Same version-skew contract as threadSettlement. */
1576
- threadPinning: Schema$1.optionalKey(Schema$1.Boolean),
1577
- /** Server understands the thread.fork command. Same version-skew contract
1578
- as threadSettlement. */
1579
- threadFork: Schema$1.optionalKey(Schema$1.Boolean),
1580
- /** Server understands thread.scheduled-task.create / cancel and fires
1581
- pending tasks. Same version-skew contract as threadSettlement. */
1582
- threadScheduledTasks: Schema$1.optionalKey(Schema$1.Boolean),
1583
- /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on
1584
- servers from before the pull-request workspace shipped, so clients must not probe them. */
1585
- pullRequests: Schema$1.optionalKey(Schema$1.Boolean),
1586
- /** Whole-host capacity snapshots for automatic draft routing. */
1587
- hostResources: Schema$1.optionalKey(Schema$1.Boolean),
1588
- /** Device open returns lease IDs and close honors expectedLeaseId. */
1589
- deviceSessionLeases: Schema$1.optionalKey(Schema$1.Boolean),
1590
- /** Manual detail/action requests can require an exact viewing lifetime. */
1591
- deviceActionLeases: Schema$1.optionalKey(Schema$1.Boolean),
1592
- /** Stack-scoped mutations preserve expected heads. Require true before sending stack payloads;
1593
- older servers may otherwise interpret the request as a single-PR action. */
1594
- pullRequestStackActions: Schema$1.optionalKey(Schema$1.Boolean),
1595
- /** The update path clients should offer for this server. Absent on
1596
- servers that must be relaunched manually (dev checkouts, Windows
1597
- foreground runs, pre-update servers). */
1598
- serverSelfUpdate: Schema$1.optionalKey(ServerSelfUpdateCapability),
1599
- /**
1600
- * Set when a service supervises the server, so a client offering a manual
1601
- * update must say `service update` rather than a bare relaunch — which would
1602
- * start a second server beside the supervised one.
1603
- *
1604
- * A new optional field rather than a new `ServerSelfUpdateCapability` member:
1605
- * adding a literal to that union makes older clients fail to decode the whole
1606
- * descriptor, while an unknown key is ignored.
1607
- */
1608
- serverServiceSupervised: Schema$1.optionalKey(Schema$1.Boolean)
1609
- });
1610
- const ExecutionEnvironmentDescriptor = Schema$1.Struct({
1611
- environmentId: EnvironmentId,
1612
- label: TrimmedNonEmptyString,
1613
- platform: ExecutionEnvironmentPlatform,
1614
- serverVersion: TrimmedNonEmptyString,
1615
- capabilities: ExecutionEnvironmentCapabilities
1616
- });
1617
- Schema$1.Literals([
1618
- "connecting",
1619
- "connected",
1620
- "disconnected",
1621
- "error"
1622
- ]);
1623
- const RepositoryIdentityLocator = Schema$1.Struct({
1624
- source: Schema$1.Literal("git-remote"),
1625
- remoteName: TrimmedNonEmptyString,
1626
- remoteUrl: TrimmedNonEmptyString
1627
- });
1628
- const RepositoryIdentity = Schema$1.Struct({
1629
- canonicalKey: TrimmedNonEmptyString,
1630
- locator: RepositoryIdentityLocator,
1631
- rootPath: Schema$1.optionalKey(TrimmedNonEmptyString),
1632
- displayName: Schema$1.optionalKey(TrimmedNonEmptyString),
1633
- provider: Schema$1.optionalKey(TrimmedNonEmptyString),
1634
- owner: Schema$1.optionalKey(TrimmedNonEmptyString),
1635
- name: Schema$1.optionalKey(TrimmedNonEmptyString)
1636
- });
1637
- Schema$1.Struct({
1638
- environmentId: EnvironmentId,
1639
- projectId: ProjectId
1640
- });
1641
- Schema$1.Struct({
1642
- environmentId: EnvironmentId,
1643
- threadId: ThreadId
1644
- });
1645
- Schema$1.Struct({
1646
- environmentId: EnvironmentId,
1647
- threadId: ThreadId
1648
- });
1649
- //#endregion
1650
1380
  //#region ../../packages/contracts/src/vcs.ts
1651
1381
  const VcsDriverKind = Schema$1.Literals([
1652
1382
  "git",
@@ -3215,6 +2945,128 @@ const MODEL_SLUG_ALIASES_BY_PROVIDER = {
3215
2945
  [OPENCODE_DRIVER_KIND]: {}
3216
2946
  };
3217
2947
  //#endregion
2948
+ //#region ../../packages/contracts/src/environment.ts
2949
+ const ExecutionEnvironmentPlatformOs = Schema$1.Literals([
2950
+ "darwin",
2951
+ "linux",
2952
+ "windows",
2953
+ "unknown"
2954
+ ]);
2955
+ const ExecutionEnvironmentPlatformArch = Schema$1.Literals([
2956
+ "arm64",
2957
+ "x64",
2958
+ "other"
2959
+ ]);
2960
+ const ExecutionEnvironmentPlatform = Schema$1.Struct({
2961
+ os: ExecutionEnvironmentPlatformOs,
2962
+ arch: ExecutionEnvironmentPlatformArch
2963
+ });
2964
+ /** How a server can replace itself with another version when asked over RPC:
2965
+ "boot-service" rewrites the systemd user unit and restarts it; "respawn"
2966
+ installs the target version and respawns the foreground process. */
2967
+ const ServerSelfUpdateMethod = Schema$1.Literals(["boot-service", "respawn"]);
2968
+ /** What update path a client should offer for a server: one of the RPC
2969
+ self-update methods above, or "desktop-managed" when the backend's
2970
+ version belongs to the P4Code desktop app supervising it — updating the
2971
+ app on that machine is the only way to update the server. */
2972
+ const ServerSelfUpdateCapability = Schema$1.Literals([
2973
+ "boot-service",
2974
+ "respawn",
2975
+ "desktop-managed"
2976
+ ]);
2977
+ const ExecutionEnvironmentCapabilities = Schema$1.Struct({
2978
+ computerUseControls: Schema$1.optionalKey(Schema$1.Boolean),
2979
+ usagePriceOverrides: Schema$1.optionalKey(Schema$1.Boolean),
2980
+ projectAutoPull: Schema$1.optionalKey(Schema$1.Boolean),
2981
+ sharedProjectModelDefault: Schema$1.optionalKey(Schema$1.Boolean),
2982
+ projectScriptDefaults: Schema$1.optionalKey(Schema$1.Boolean),
2983
+ customModelDefinitions: Schema$1.optionalKey(Schema$1.Boolean),
2984
+ threadLifecycleV2: Schema$1.optionalKey(Schema$1.Boolean),
2985
+ repositoryIdentity: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
2986
+ connectionProbe: Schema$1.optionalKey(Schema$1.Boolean),
2987
+ /** Server understands thread.settle / thread.unsettle commands. Absent on
2988
+ pre-settlement servers, so clients treat missing as unsupported and
2989
+ never send the commands under version skew. */
2990
+ threadSettlement: Schema$1.optionalKey(Schema$1.Boolean),
2991
+ /** Server understands thread.snooze / thread.unsnooze commands. Same
2992
+ version-skew contract as threadSettlement. */
2993
+ threadSnooze: Schema$1.optionalKey(Schema$1.Boolean),
2994
+ /** Server understands thread.pin / thread.unpin / thread.pin.reorder
2995
+ commands. Same version-skew contract as threadSettlement. */
2996
+ threadPinning: Schema$1.optionalKey(Schema$1.Boolean),
2997
+ /** Server understands the thread.fork command. Same version-skew contract
2998
+ as threadSettlement. */
2999
+ threadFork: Schema$1.optionalKey(Schema$1.Boolean),
3000
+ /** Server understands thread.scheduled-task.create / cancel and fires
3001
+ pending tasks. Same version-skew contract as threadSettlement. */
3002
+ threadScheduledTasks: Schema$1.optionalKey(Schema$1.Boolean),
3003
+ /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on
3004
+ servers from before the pull-request workspace shipped, so clients must not probe them. */
3005
+ pullRequests: Schema$1.optionalKey(Schema$1.Boolean),
3006
+ /** Whole-host capacity snapshots for automatic draft routing. */
3007
+ hostResources: Schema$1.optionalKey(Schema$1.Boolean),
3008
+ /** Device open returns lease IDs and close honors expectedLeaseId. */
3009
+ deviceSessionLeases: Schema$1.optionalKey(Schema$1.Boolean),
3010
+ /** Manual detail/action requests can require an exact viewing lifetime. */
3011
+ deviceActionLeases: Schema$1.optionalKey(Schema$1.Boolean),
3012
+ /** Stack-scoped mutations preserve expected heads. Require true before sending stack payloads;
3013
+ older servers may otherwise interpret the request as a single-PR action. */
3014
+ pullRequestStackActions: Schema$1.optionalKey(Schema$1.Boolean),
3015
+ /** The update path clients should offer for this server. Absent on
3016
+ servers that must be relaunched manually (dev checkouts, Windows
3017
+ foreground runs, pre-update servers). */
3018
+ serverSelfUpdate: Schema$1.optionalKey(ServerSelfUpdateCapability),
3019
+ /**
3020
+ * Set when a service supervises the server, so a client offering a manual
3021
+ * update must say `service update` rather than a bare relaunch — which would
3022
+ * start a second server beside the supervised one.
3023
+ *
3024
+ * A new optional field rather than a new `ServerSelfUpdateCapability` member:
3025
+ * adding a literal to that union makes older clients fail to decode the whole
3026
+ * descriptor, while an unknown key is ignored.
3027
+ */
3028
+ serverServiceSupervised: Schema$1.optionalKey(Schema$1.Boolean)
3029
+ });
3030
+ const ExecutionEnvironmentDescriptor = Schema$1.Struct({
3031
+ environmentId: EnvironmentId,
3032
+ label: TrimmedNonEmptyString,
3033
+ platform: ExecutionEnvironmentPlatform,
3034
+ serverVersion: TrimmedNonEmptyString,
3035
+ capabilities: ExecutionEnvironmentCapabilities
3036
+ });
3037
+ Schema$1.Literals([
3038
+ "connecting",
3039
+ "connected",
3040
+ "disconnected",
3041
+ "error"
3042
+ ]);
3043
+ const RepositoryIdentityLocator = Schema$1.Struct({
3044
+ source: Schema$1.Literal("git-remote"),
3045
+ remoteName: TrimmedNonEmptyString,
3046
+ remoteUrl: TrimmedNonEmptyString
3047
+ });
3048
+ const RepositoryIdentity = Schema$1.Struct({
3049
+ canonicalKey: TrimmedNonEmptyString,
3050
+ locator: RepositoryIdentityLocator,
3051
+ rootPath: Schema$1.optionalKey(TrimmedNonEmptyString),
3052
+ displayName: Schema$1.optionalKey(TrimmedNonEmptyString),
3053
+ provider: Schema$1.optionalKey(TrimmedNonEmptyString),
3054
+ owner: Schema$1.optionalKey(TrimmedNonEmptyString),
3055
+ name: Schema$1.optionalKey(TrimmedNonEmptyString)
3056
+ });
3057
+ Schema$1.Struct({
3058
+ environmentId: EnvironmentId,
3059
+ projectId: ProjectId
3060
+ });
3061
+ Schema$1.Struct({
3062
+ environmentId: EnvironmentId,
3063
+ threadId: ThreadId
3064
+ });
3065
+ Schema$1.Struct({
3066
+ environmentId: EnvironmentId,
3067
+ threadId: ThreadId
3068
+ });
3069
+ //#endregion
3218
3070
  //#region ../../packages/contracts/src/orchestration.ts
3219
3071
  const ORCHESTRATION_WS_METHODS = {
3220
3072
  dispatchCommand: "orchestration.dispatchCommand",
@@ -3296,6 +3148,12 @@ const COMPRESS_MODE_VALUES = [
3296
3148
  "ultra",
3297
3149
  "wenyan"
3298
3150
  ];
3151
+ /**
3152
+ * The strict mode set, for a reader that must not accept a mode it cannot
3153
+ * name. `CompressMode` below stays tolerant for wire compatibility; anything
3154
+ * storing a mode as a fact uses this one instead, because coercing an unknown
3155
+ * mode to the default would record a compression pass that never happened.
3156
+ */
3299
3157
  const CompressModeLiteral = Schema$1.Literals(COMPRESS_MODE_VALUES);
3300
3158
  /**
3301
3159
  * Tolerant on the wire: an unknown mode decodes to the default instead of
@@ -5347,6 +5205,579 @@ var OrchestrationSearchThreadsError = class extends Schema$1.TaggedErrorClass()(
5347
5205
  message: TrimmedNonEmptyString,
5348
5206
  cause: Schema$1.optional(Schema$1.Defect())
5349
5207
  }) {};
5208
+ const ThreadWatchEventsInput = Schema$1.Struct({
5209
+ threadId: ThreadId.annotate({ description: "The thread to read events from. Any thread on this server, not only this session's own." }),
5210
+ afterSequence: Schema$1.optional(NonNegativeInt.annotate({ description: "Exclusive sequence cursor. Events with a higher sequence are returned. Omit to read from the beginning." })),
5211
+ limit: Schema$1.optional(NonNegativeInt.check(Schema$1.isLessThanOrEqualTo(200)).annotate({ description: `Maximum events to return (default 50, max 200).` }))
5212
+ });
5213
+ const ThreadWatchEventsResult = Schema$1.Struct({
5214
+ threadId: ThreadId,
5215
+ /** Ordered by sequence ascending; resume with the last event's sequence. */
5216
+ events: Schema$1.Array(OrchestrationEvent),
5217
+ /**
5218
+ * The engine's latest persisted sequence at read time. Equal to the last
5219
+ * returned event's sequence when the watcher is caught up.
5220
+ */
5221
+ headSequence: NonNegativeInt,
5222
+ /** True when more events for this thread exist past the returned page. */
5223
+ hasMore: Schema$1.Boolean
5224
+ });
5225
+ /** Compact review limits are server-owned, independent of raw event paging. */
5226
+ const THREAD_REVIEW_SCAN_LIMIT = 2e3;
5227
+ const THREAD_REVIEW_ITEM_MAX_BYTES = 8e3;
5228
+ const ThreadWatchReviewInput = Schema$1.Struct({
5229
+ threadId: ThreadId,
5230
+ afterSequence: Schema$1.optional(NonNegativeInt),
5231
+ throughSequence: Schema$1.optional(NonNegativeInt.annotate({ description: "Inclusive fixed review boundary. Omit on the first page to capture the head; reuse the returned boundary on every subsequent page." }))
5232
+ });
5233
+ const ReviewSourceFields = {
5234
+ firstSequence: NonNegativeInt,
5235
+ lastSequence: NonNegativeInt,
5236
+ truncated: Schema$1.Boolean
5237
+ };
5238
+ const ThreadReviewItem = Schema$1.Union([Schema$1.Struct({
5239
+ ...ReviewSourceFields,
5240
+ kind: Schema$1.Literal("message"),
5241
+ messageId: Schema$1.String,
5242
+ turnId: Schema$1.NullOr(Schema$1.String),
5243
+ role: Schema$1.String,
5244
+ /** Apply append/replace by message identity across page boundaries. */
5245
+ operation: Schema$1.Literals(["append", "replace"]),
5246
+ complete: Schema$1.Boolean,
5247
+ text: Schema$1.String,
5248
+ /**
5249
+ * What the message carried besides text.
5250
+ *
5251
+ * Present only when there was something: a message whose fragments are
5252
+ * merged by identity would otherwise lose every trace of an image or a
5253
+ * document the builder attached. Bounded by
5254
+ * {@link THREAD_REVIEW_ITEM_MAX_ATTACHMENTS} so the item stays trimmable -
5255
+ * only its text shrinks under the byte cap, so nothing else may be able to
5256
+ * grow without limit.
5257
+ */
5258
+ attachments: Schema$1.optionalKey(Schema$1.Array(Schema$1.Struct({
5259
+ type: Schema$1.String,
5260
+ name: Schema$1.String,
5261
+ sizeBytes: NonNegativeInt
5262
+ })))
5263
+ }), Schema$1.Struct({
5264
+ ...ReviewSourceFields,
5265
+ kind: Schema$1.Literal("event"),
5266
+ type: Schema$1.String,
5267
+ /** JSON evidence excerpt. Truncated excerpts require targeted raw recovery. */
5268
+ payload: Schema$1.String
5269
+ })]);
5270
+ /**
5271
+ * Compressed evidence standing in for the items it replaces.
5272
+ *
5273
+ * The supervisor reads this instead of paging the range event by event. The
5274
+ * sequence bounds are what make that safe: any claim can be verified at source
5275
+ * with a targeted `thread_watch_events` read over exactly this range, so the
5276
+ * summary is a starting point rather than the only record.
5277
+ *
5278
+ * `degraded` is set whenever the items came back unsummarized, and names why.
5279
+ * Most reasons are failures - unset model, provider error, timeout, empty or
5280
+ * unusable output - but `page-under-threshold` is not: it means the page was
5281
+ * small enough that summarizing it would have cost more than reading it. The
5282
+ * supervisor is never left guessing whether it is reading everything.
5283
+ */
5284
+ const ThreadReviewSummary = Schema$1.Struct({
5285
+ text: Schema$1.String,
5286
+ firstSequence: NonNegativeInt,
5287
+ lastSequence: NonNegativeInt,
5288
+ /** How many builder turns the range covered. */
5289
+ turnCount: NonNegativeInt,
5290
+ degraded: Schema$1.NullOr(Schema$1.String)
5291
+ });
5292
+ const ThreadWatchReviewResult = Schema$1.Struct({
5293
+ threadId: ThreadId,
5294
+ items: Schema$1.Array(ThreadReviewItem),
5295
+ throughSequence: NonNegativeInt,
5296
+ /** Resume from the source scan cursor, even when items is empty. */
5297
+ nextAfterSequence: NonNegativeInt,
5298
+ hasMore: Schema$1.Boolean,
5299
+ scannedEvents: NonNegativeInt,
5300
+ /**
5301
+ * Present only when summarization ran. When its `degraded` is null the
5302
+ * `items` for the summarized range are omitted and this stands in for them;
5303
+ * otherwise the items are intact and this explains why.
5304
+ */
5305
+ summary: Schema$1.optionalKey(Schema$1.NullOr(ThreadReviewSummary))
5306
+ });
5307
+ /**
5308
+ * How a review ended, from the supervisor's own account of it.
5309
+ *
5310
+ * Three states rather than a boolean because they are acted on differently: a
5311
+ * summary that arrived is evidence, a fallback is a working review that cost
5312
+ * full context, and a failure is a misconfiguration worth fixing. The fourth
5313
+ * state - a read no report ever answered - is not sayable here: only the
5314
+ * server can observe it, through `ThreadEvidenceActivityOutcome`.
5315
+ */
5316
+ const ThreadEvidenceReportOutcome = Schema$1.Literals([
5317
+ "summarized",
5318
+ "fallback",
5319
+ "failed"
5320
+ ]);
5321
+ /** Bounded so one report cannot dominate a thread's activity storage. */
5322
+ const THREAD_EVIDENCE_REPORT_MAX_CHARS = 8e3;
5323
+ const ThreadEvidenceReportInput = Schema$1.Struct({
5324
+ /** The watched thread the summary covers, not the reporting thread. */
5325
+ threadId: ThreadId,
5326
+ firstSequence: NonNegativeInt,
5327
+ lastSequence: NonNegativeInt,
5328
+ /**
5329
+ * The fixed review boundary the pages were read against.
5330
+ *
5331
+ * Identity, not description: the evidence-read rows are keyed on it, so a
5332
+ * report that guessed from its own last sequence would look for a baseline
5333
+ * that never existed whenever the range ended on a filtered event.
5334
+ */
5335
+ throughSequence: NonNegativeInt,
5336
+ outcome: ThreadEvidenceReportOutcome,
5337
+ /** The summarizer's exact text. Required when the outcome is summarized. */
5338
+ summary: Schema$1.optional(Schema$1.String),
5339
+ /** The model the subagent ran on, when one was spawned. */
5340
+ model: Schema$1.optional(Schema$1.String),
5341
+ /** Why the summary is missing or degraded. Required unless summarized. */
5342
+ reason: Schema$1.optional(Schema$1.String)
5343
+ });
5344
+ /**
5345
+ * The supervisor's own context around the read, correlated by the server.
5346
+ *
5347
+ * Self-reported numbers would be worth little, so these are read from the
5348
+ * reporting thread's projected context-window rows: `before` is what was
5349
+ * recorded when the evidence read happened, `after` what is recorded when the
5350
+ * report lands. `delta` can be negative when a compaction ran in between, and
5351
+ * is null whenever no read-time baseline exists.
5352
+ */
5353
+ const ThreadEvidenceReportContext = Schema$1.Struct({
5354
+ /**
5355
+ * Null when no reading was recorded at read time.
5356
+ *
5357
+ * Never the report-time value standing in for itself: a delta of zero read
5358
+ * as measurement would be a lie about a review that cost context.
5359
+ */
5360
+ usedTokensBefore: Schema$1.NullOr(NonNegativeInt),
5361
+ usedTokensAfter: NonNegativeInt,
5362
+ /** Null exactly when `usedTokensBefore` is. */
5363
+ deltaTokens: Schema$1.NullOr(Schema$1.Number),
5364
+ maxTokens: Schema$1.NullOr(NonNegativeInt),
5365
+ capacityPercent: Schema$1.NullOr(Schema$1.Number)
5366
+ });
5367
+ /**
5368
+ * How the recorded row ended, which is the supervisor's three outcomes plus the
5369
+ * one only the server can state: the review closed with no report at all.
5370
+ *
5371
+ * `unreported` is never accepted from a tool call. The server writes it when a
5372
+ * supervisor turn ends over an evidence read that nothing answered, so the
5373
+ * absence survives outside a live client that could infer it from two rows.
5374
+ */
5375
+ const ThreadEvidenceActivityOutcome = Schema$1.Literals([
5376
+ "summarized",
5377
+ "fallback",
5378
+ "failed",
5379
+ "unreported"
5380
+ ]);
5381
+ /**
5382
+ * Server clock around one review, never the supervisor's own account of it.
5383
+ *
5384
+ * `readAt` is when the evidence-read row was recorded and `closedAt` when the
5385
+ * row that closes the review was: the report for a reported review, the
5386
+ * completed supervisor turn for an unreported one. A supervisor turn that
5387
+ * failed or was interrupted closes nothing, so its read keeps no timing until
5388
+ * some later turn reports on it.
5389
+ */
5390
+ const ThreadEvidenceReportTiming = Schema$1.Struct({
5391
+ readAt: IsoDateTime,
5392
+ closedAt: IsoDateTime,
5393
+ durationMs: NonNegativeInt
5394
+ });
5395
+ /** Payload of the single `fusion.evidence.summary` activity a report records. */
5396
+ const ThreadEvidenceReportActivity = Schema$1.Struct({
5397
+ watchedThreadId: ThreadId,
5398
+ firstSequence: NonNegativeInt,
5399
+ lastSequence: NonNegativeInt,
5400
+ throughSequence: NonNegativeInt,
5401
+ outcome: ThreadEvidenceActivityOutcome,
5402
+ summary: Schema$1.NullOr(Schema$1.String),
5403
+ /** True when a snapshot dropped the text of a superseded report. */
5404
+ summaryDropped: Schema$1.optionalKey(Schema$1.Boolean),
5405
+ model: Schema$1.NullOr(Schema$1.String),
5406
+ reason: Schema$1.NullOr(Schema$1.String),
5407
+ /** Null when no context-window row exists yet for the reporting thread. */
5408
+ context: Schema$1.NullOr(ThreadEvidenceReportContext),
5409
+ /**
5410
+ * Null when the evidence-read row behind this review is no longer readable,
5411
+ * and absent on rows stored before the server measured a review at all.
5412
+ */
5413
+ timing: Schema$1.optionalKey(Schema$1.NullOr(ThreadEvidenceReportTiming))
5414
+ });
5415
+ const ThreadEvidenceReportResult = Schema$1.Struct({
5416
+ recorded: Schema$1.Boolean,
5417
+ context: Schema$1.NullOr(ThreadEvidenceReportContext)
5418
+ });
5419
+ var WatchToolUnavailableError = class extends Schema$1.TaggedErrorClass()("WatchToolUnavailableError", {
5420
+ capability: Schema$1.Literal("watch"),
5421
+ environmentId: EnvironmentId,
5422
+ threadId: ThreadId,
5423
+ providerSessionId: TrimmedNonEmptyString,
5424
+ providerInstanceId: ProviderInstanceId
5425
+ }) {
5426
+ get message() {
5427
+ return `MCP credential does not grant the ${this.capability} capability.`;
5428
+ }
5429
+ };
5430
+ /**
5431
+ * The capability is present but the requested thread is not in the granted
5432
+ * set. Separate from {@link WatchToolUnavailableError} so a watcher can tell
5433
+ * "you cannot watch at all" apart from "not this thread".
5434
+ */
5435
+ var ThreadWatchNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadWatchNotPermittedError", { threadId: ThreadId }) {
5436
+ get message() {
5437
+ return `This session was not granted watch access to thread ${this.threadId}.`;
5438
+ }
5439
+ };
5440
+ var ThreadWatchFailedError = class extends Schema$1.TaggedErrorClass()("ThreadWatchFailedError", {
5441
+ threadId: ThreadId,
5442
+ detail: Schema$1.String
5443
+ }) {
5444
+ get message() {
5445
+ return `Reading events for thread ${this.threadId} failed: ${this.detail}`;
5446
+ }
5447
+ };
5448
+ const ThreadWatchToolError = Schema$1.Union([
5449
+ WatchToolUnavailableError,
5450
+ ThreadWatchNotPermittedError,
5451
+ ThreadWatchFailedError
5452
+ ]);
5453
+ //#endregion
5454
+ //#region ../../packages/contracts/src/hubAnalytics.ts
5455
+ /**
5456
+ * Account-scoped analytics facts the hub stores for its own account holder.
5457
+ *
5458
+ * Append-only and versioned: a fact is what happened once, never a row that is
5459
+ * later corrected, so a changed shape ships as a new `version` beside the old
5460
+ * one rather than a rewrite of stored rows. Every payload is bounded, because
5461
+ * these travel from environments the hub does not control and a fact that can
5462
+ * grow without limit is a store that can be filled by one caller.
5463
+ *
5464
+ * Nothing here leaves the account: the hub is the only sink, and no field is
5465
+ * shaped for an external telemetry vendor.
5466
+ *
5467
+ * @module hubAnalytics
5468
+ */
5469
+ const MAX_FACT_ID_CHARS = 128;
5470
+ const MAX_QUESTION_CHARS$2 = 2e3;
5471
+ const MAX_HEADER_CHARS$2 = 120;
5472
+ const MAX_OPTION_LABEL_CHARS$1 = 200;
5473
+ const MAX_OPTION_DESCRIPTION_CHARS$1 = 600;
5474
+ const MAX_PRESENTED_OPTIONS$1 = 12;
5475
+ const MAX_CUSTOM_ANSWER_CHARS$1 = 2e3;
5476
+ const MAX_SELECTED_ANSWERS = MAX_PRESENTED_OPTIONS$1;
5477
+ const MAX_VERSION_CHARS = 40;
5478
+ const MAX_REASON_CHARS$1 = 200;
5479
+ const MAX_MODEL_CHARS$1 = 120;
5480
+ const MAX_QUERY_KINDS = 3;
5481
+ const MAX_QUERY_LIMIT = 200;
5482
+ const DEFAULT_QUERY_LIMIT = 50;
5483
+ const MAX_CURSOR_CHARS = 160;
5484
+ const factId = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_FACT_ID_CHARS), Schema$1.isPattern(/^[A-Za-z0-9][A-Za-z0-9._:-]*$(?![\s\S])/));
5485
+ const nonNegativeInt$1 = Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(0));
5486
+ /**
5487
+ * One option exactly as the user saw it, so a later preference read has its
5488
+ * context. Deliberately its own shape rather than the live question contract:
5489
+ * a fact records what was shown that day, and must not change meaning when the
5490
+ * question schema does.
5491
+ */
5492
+ const PresentedOption = Schema$1.Struct({
5493
+ label: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_OPTION_LABEL_CHARS$1)),
5494
+ description: Schema$1.String.check(Schema$1.isMaxLength(MAX_OPTION_DESCRIPTION_CHARS$1))
5495
+ });
5496
+ /**
5497
+ * What the user actually answered.
5498
+ *
5499
+ * The custom text is kept verbatim: an answer nobody offered is the most
5500
+ * informative one there is, and summarizing it away would leave the fact
5501
+ * saying only that the offered options were all wrong.
5502
+ */
5503
+ const QuestionAnswer = Schema$1.Union([
5504
+ Schema$1.Struct({
5505
+ kind: Schema$1.Literal("option"),
5506
+ optionIndexes: Schema$1.Array(nonNegativeInt$1).check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_SELECTED_ANSWERS)),
5507
+ labels: Schema$1.Array(Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_OPTION_LABEL_CHARS$1))).check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_SELECTED_ANSWERS))
5508
+ }),
5509
+ Schema$1.Struct({
5510
+ kind: Schema$1.Literal("custom"),
5511
+ text: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_CUSTOM_ANSWER_CHARS$1))
5512
+ }),
5513
+ Schema$1.Struct({ kind: Schema$1.Literal("dismissed") })
5514
+ ]);
5515
+ /** An `ask_user_question` as presented, and what came back. */
5516
+ const HubQuestionFactV1 = Schema$1.Struct({
5517
+ questionId: factId,
5518
+ threadId: Schema$1.optional(ThreadId),
5519
+ header: Schema$1.String.check(Schema$1.isMaxLength(MAX_HEADER_CHARS$2)),
5520
+ question: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_QUESTION_CHARS$2)),
5521
+ multiSelect: Schema$1.Boolean,
5522
+ options: Schema$1.Array(PresentedOption).check(Schema$1.isMaxLength(MAX_PRESENTED_OPTIONS$1)),
5523
+ answer: QuestionAnswer,
5524
+ answeredInMs: Schema$1.optional(nonNegativeInt$1)
5525
+ });
5526
+ /**
5527
+ * One Fusion handoff, from one implementer thread.
5528
+ *
5529
+ * Efficiency is reported as what the handoff carried and what it cost, never
5530
+ * as a score: the counts are comparable across versions, a verdict is not.
5531
+ */
5532
+ const HubFusionHandoffFactV1 = Schema$1.Struct({
5533
+ fusionVersion: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_VERSION_CHARS)),
5534
+ threadId: ThreadId,
5535
+ /**
5536
+ * The recorded review outcome, in the server's own vocabulary: `unreported`
5537
+ * is a real result - a review that closed answering nothing - and collapsing
5538
+ * it into a failure would lose the only row that says so.
5539
+ */
5540
+ status: ThreadEvidenceActivityOutcome,
5541
+ /** The model the report names, or null when the review recorded none. */
5542
+ model: Schema$1.NullOr(Schema$1.String.check(Schema$1.isMaxLength(MAX_MODEL_CHARS$1))),
5543
+ reason: Schema$1.NullOr(Schema$1.String.check(Schema$1.isMaxLength(MAX_REASON_CHARS$1))),
5544
+ transcriptEvents: nonNegativeInt$1,
5545
+ transcriptIncluded: nonNegativeInt$1,
5546
+ transcriptDropped: nonNegativeInt$1,
5547
+ transcriptClippedFields: nonNegativeInt$1,
5548
+ ledgerRows: nonNegativeInt$1,
5549
+ ledgerTotalRows: nonNegativeInt$1,
5550
+ summaryChars: Schema$1.NullOr(nonNegativeInt$1),
5551
+ contextDeltaTokens: Schema$1.NullOr(Schema$1.Int),
5552
+ durationMs: Schema$1.NullOr(nonNegativeInt$1)
5553
+ });
5554
+ /**
5555
+ * One compression pass: what it started from, what it left, and what it cost.
5556
+ *
5557
+ * The mode is the product's own strict mode set, so a mode added there is
5558
+ * recordable here the day it ships; a local copy of the literals would quietly
5559
+ * reject the new one and lose exactly the passes worth measuring. The strict
5560
+ * schema rather than the tolerant wire one: a fact that relabelled an unknown
5561
+ * mode as the default would be a record of something that never ran.
5562
+ */
5563
+ const HubCompressionFactV1 = Schema$1.Struct({
5564
+ mode: CompressModeLiteral,
5565
+ threadId: Schema$1.optional(ThreadId),
5566
+ beforeTokens: nonNegativeInt$1,
5567
+ afterTokens: nonNegativeInt$1,
5568
+ /** Negative when the pass cost more than it saved; the sign is the finding. */
5569
+ deltaTokens: Schema$1.Int,
5570
+ durationMs: nonNegativeInt$1
5571
+ });
5572
+ const HubAnalyticsFactKind = Schema$1.Literals([
5573
+ "question.answered",
5574
+ "fusion.handoff",
5575
+ "compression.applied"
5576
+ ]);
5577
+ const factBase = {
5578
+ /** Dedupe identity: the same id appended twice stores one fact. */
5579
+ eventId: factId,
5580
+ occurredAt: IsoDateTime
5581
+ };
5582
+ /** One append. The kind fixes the payload shape, and the version fixes its meaning. */
5583
+ const HubAnalyticsFactInput = Schema$1.Union([
5584
+ Schema$1.Struct({
5585
+ ...factBase,
5586
+ kind: Schema$1.Literal("question.answered"),
5587
+ version: Schema$1.Literal(1),
5588
+ payload: HubQuestionFactV1
5589
+ }),
5590
+ Schema$1.Struct({
5591
+ ...factBase,
5592
+ kind: Schema$1.Literal("fusion.handoff"),
5593
+ version: Schema$1.Literal(1),
5594
+ payload: HubFusionHandoffFactV1
5595
+ }),
5596
+ Schema$1.Struct({
5597
+ ...factBase,
5598
+ kind: Schema$1.Literal("compression.applied"),
5599
+ version: Schema$1.Literal(1),
5600
+ payload: HubCompressionFactV1
5601
+ })
5602
+ ]);
5603
+ /** A stored fact as it is read back, with the environment that reported it. */
5604
+ const HubAnalyticsFact = Schema$1.Struct({
5605
+ eventId: factId,
5606
+ environmentId: EnvironmentId,
5607
+ kind: HubAnalyticsFactKind,
5608
+ version: Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(1)),
5609
+ occurredAt: IsoDateTime,
5610
+ payload: Schema$1.Unknown
5611
+ });
5612
+ /**
5613
+ * An opaque page position: the occurrence millisecond and the event id that
5614
+ * produced it, which together order every row exactly once.
5615
+ */
5616
+ const HubAnalyticsCursor = Schema$1.String.check(Schema$1.isMaxLength(MAX_CURSOR_CHARS), Schema$1.isPattern(/^\d+\|[A-Za-z0-9][A-Za-z0-9._:-]*$(?![\s\S])/));
5617
+ const HubAnalyticsQuery = Schema$1.Struct({
5618
+ kinds: Schema$1.optional(Schema$1.Array(HubAnalyticsFactKind).check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_QUERY_KINDS))),
5619
+ since: Schema$1.optional(IsoDateTime),
5620
+ until: Schema$1.optional(IsoDateTime),
5621
+ limit: Schema$1.optional(Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(1), Schema$1.isLessThanOrEqualTo(MAX_QUERY_LIMIT))),
5622
+ cursor: Schema$1.optional(HubAnalyticsCursor)
5623
+ });
5624
+ const HubAnalyticsPage = Schema$1.Struct({
5625
+ facts: Schema$1.Array(HubAnalyticsFact),
5626
+ /** Null when the page reached the end, never an empty string. */
5627
+ nextCursor: Schema$1.NullOr(HubAnalyticsCursor)
5628
+ });
5629
+ const HUB_ANALYTICS_QUERY_LIMITS = {
5630
+ defaultLimit: DEFAULT_QUERY_LIMIT,
5631
+ maxLimit: MAX_QUERY_LIMIT
5632
+ };
5633
+ //#endregion
5634
+ //#region ../../packages/contracts/src/hubNotifications.ts
5635
+ const MAX_NOTIFICATION_ID_CHARS = 128;
5636
+ const MAX_PUSH_TOKEN_CHARS = 4096;
5637
+ const MAX_HUB_ORIGIN_CHARS = 2048;
5638
+ const MAX_ACTIVITY_TITLE_CHARS = 160;
5639
+ const MAX_ACTIVITY_DETAIL_CHARS = 240;
5640
+ const identifier = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_NOTIFICATION_ID_CHARS), Schema$1.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$(?![\s\S])/));
5641
+ const pushToken = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PUSH_TOKEN_CHARS), Schema$1.isPattern(/^[\x21-\x7e]+$(?![\s\S])/));
5642
+ /** Server emission is bound to the authenticated environment, never a supplied account. */
5643
+ const HubAgentActivityInput = Schema$1.Struct({
5644
+ eventId: identifier,
5645
+ threadId: ThreadId,
5646
+ revision: Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(0)),
5647
+ state: Schema$1.Literals([
5648
+ "running",
5649
+ "approval_required",
5650
+ "question_required",
5651
+ "completed",
5652
+ "failed",
5653
+ "stopped"
5654
+ ]),
5655
+ title: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_ACTIVITY_TITLE_CHARS)),
5656
+ detail: Schema$1.String.check(Schema$1.isMaxLength(MAX_ACTIVITY_DETAIL_CHARS))
5657
+ });
5658
+ /** Each authenticated client owns its registration; tokens never belong in public client state. */
5659
+ const HubPushRegistrationInput = Schema$1.Union([Schema$1.Struct({
5660
+ platform: Schema$1.Literal("ios"),
5661
+ token: pushToken,
5662
+ sandbox: Schema$1.Boolean
5663
+ }), Schema$1.Struct({
5664
+ platform: Schema$1.Literal("android"),
5665
+ token: pushToken
5666
+ })]);
5667
+ /** Explicit opt-in, independently reversible. Omission never implies permission. */
5668
+ const HubNotificationPreferences = Schema$1.Struct({
5669
+ attention: Schema$1.Boolean,
5670
+ completion: Schema$1.Boolean,
5671
+ liveActivity: Schema$1.Boolean
5672
+ });
5673
+ Schema$1.Struct({
5674
+ registered: Schema$1.Boolean,
5675
+ registrationGeneration: Schema$1.NullOr(identifier),
5676
+ preferences: HubNotificationPreferences
5677
+ });
5678
+ /** ActivityKit update token, distinct from the app's APNs registration token. */
5679
+ const HubActivityOwnership = Schema$1.Struct({
5680
+ origin: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_HUB_ORIGIN_CHARS)),
5681
+ environmentId: EnvironmentId,
5682
+ clientId: HubClientId,
5683
+ registrationGeneration: identifier,
5684
+ threadId: ThreadId,
5685
+ activityId: identifier
5686
+ });
5687
+ const HubLiveActivityRegistrationInput = Schema$1.Struct({
5688
+ threadId: ThreadId,
5689
+ registrationGeneration: identifier,
5690
+ activityId: identifier,
5691
+ token: Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PUSH_TOKEN_CHARS), Schema$1.isPattern(/^(?:[a-fA-F0-9]{2})+$(?![\s\S])/)),
5692
+ sandbox: Schema$1.Boolean,
5693
+ p4Ownership: Schema$1.optional(HubActivityOwnership)
5694
+ });
5695
+ /** Close only this presentation; delayed requests must not retire a newer activity. */
5696
+ const HubLiveActivityRemoveInput = Schema$1.Struct({
5697
+ threadId: ThreadId,
5698
+ registrationGeneration: identifier,
5699
+ activityId: identifier
5700
+ });
5701
+ const HUB_TUNNEL_WEBSOCKET_PROTOCOL = "p4-hub-tunnel-v1";
5702
+ const HUB_TUNNEL_INITIAL_WINDOW_BYTES = 256 * 1024;
5703
+ const HUB_TUNNEL_MAX_WINDOW_BYTES = 1024 * 1024;
5704
+ const MAX_HEADERS = 64;
5705
+ const MAX_HEADER_NAME_CHARS = 128;
5706
+ const MAX_HEADER_VALUE_CHARS = 8192;
5707
+ const MAX_PATH_CHARS = 8192;
5708
+ const MAX_PROTOCOLS = 16;
5709
+ const MAX_PROTOCOL_CHARS = 128;
5710
+ const HeaderName = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_HEADER_NAME_CHARS), Schema$1.isPattern(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$(?![\s\S])/));
5711
+ const HeaderValue = Schema$1.String.check(Schema$1.isMaxLength(MAX_HEADER_VALUE_CHARS), Schema$1.isPattern(/^[\t\x20-\x7e]*$(?![\s\S])/));
5712
+ const Headers$2 = Schema$1.Array(Schema$1.Tuple([HeaderName, HeaderValue])).check(Schema$1.isMaxLength(MAX_HEADERS));
5713
+ const Path = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PATH_CHARS), Schema$1.isPattern(/^\/(?!\/)[^\s\\#\x00-\x1f\x7f]*$(?![\s\S])/));
5714
+ const Protocol = Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(MAX_PROTOCOL_CHARS), Schema$1.isPattern(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$(?![\s\S])/));
5715
+ const FailureReason = Schema$1.Literals([
5716
+ "cancelled",
5717
+ "unavailable",
5718
+ "protocol_error"
5719
+ ]);
5720
+ const CloseCode = Schema$1.Int.check(Schema$1.makeFilter((code) => code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006 || code >= 3e3 && code <= 4999));
5721
+ /** No caller-supplied account, environment, origin, or connector ownership authority. */
5722
+ const HubTunnelControl = Schema$1.Union([
5723
+ Schema$1.Struct({
5724
+ type: Schema$1.Literal("connector_ready"),
5725
+ protocolVersion: Schema$1.Literal(1)
5726
+ }),
5727
+ Schema$1.Struct({
5728
+ type: Schema$1.Literal("http_request_start"),
5729
+ method: Schema$1.Literals([
5730
+ "GET",
5731
+ "HEAD",
5732
+ "POST",
5733
+ "PUT",
5734
+ "PATCH",
5735
+ "DELETE",
5736
+ "OPTIONS"
5737
+ ]),
5738
+ path: Path,
5739
+ headers: Headers$2
5740
+ }),
5741
+ Schema$1.Struct({ type: Schema$1.Literal("http_request_end") }),
5742
+ Schema$1.Struct({
5743
+ type: Schema$1.Literal("http_request_abort"),
5744
+ reason: FailureReason
5745
+ }),
5746
+ Schema$1.Struct({
5747
+ type: Schema$1.Literal("http_response_start"),
5748
+ status: Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(200), Schema$1.isLessThanOrEqualTo(599)),
5749
+ headers: Headers$2
5750
+ }),
5751
+ Schema$1.Struct({ type: Schema$1.Literal("http_response_end") }),
5752
+ Schema$1.Struct({
5753
+ type: Schema$1.Literal("http_response_abort"),
5754
+ reason: FailureReason
5755
+ }),
5756
+ Schema$1.Struct({
5757
+ type: Schema$1.Literal("websocket_open"),
5758
+ path: Path,
5759
+ headers: Headers$2,
5760
+ protocols: Schema$1.Array(Protocol).check(Schema$1.isMaxLength(MAX_PROTOCOLS))
5761
+ }),
5762
+ Schema$1.Struct({
5763
+ type: Schema$1.Literal("websocket_accept"),
5764
+ protocol: Schema$1.optional(Protocol)
5765
+ }),
5766
+ Schema$1.Struct({
5767
+ type: Schema$1.Literal("websocket_reject"),
5768
+ status: Schema$1.Int.check(Schema$1.isGreaterThanOrEqualTo(400), Schema$1.isLessThanOrEqualTo(599)),
5769
+ reason: FailureReason
5770
+ }),
5771
+ Schema$1.Struct({
5772
+ type: Schema$1.Literal("websocket_close"),
5773
+ code: CloseCode,
5774
+ reason: FailureReason
5775
+ }),
5776
+ Schema$1.Struct({
5777
+ type: Schema$1.Literal("window_update"),
5778
+ creditBytes: Schema$1.Int.check(Schema$1.isGreaterThan(0), Schema$1.isLessThanOrEqualTo(HUB_TUNNEL_MAX_WINDOW_BYTES))
5779
+ })
5780
+ ]);
5350
5781
  //#endregion
5351
5782
  //#region ../../packages/contracts/src/environmentHttp.ts
5352
5783
  const OptionalBearerHeaders = Schema$1.Struct({
@@ -7866,9 +8297,16 @@ const UserInputResolvedPayload = Schema$1.Struct({ answers: UnknownRecordSchema
7866
8297
  * that outlives its turn is in practice a watch loop). Canonical single copy,
7867
8298
  * so the server liveness registry and any client fold classify identically.
7868
8299
  */
8300
+ const MONITOR_TOOL_TASK_TYPES = /* @__PURE__ */ new Set(["monitor", "monitor_mcp"]);
8301
+ /**
8302
+ * A watch loop the agent asked for by name, without the background shells.
8303
+ *
8304
+ * A backgrounded shell is only a watch loop in practice; it can equally run a
8305
+ * build or edit a file, so a consumer that needs proof of waiting rather than
8306
+ * a liveness hint must use this narrower set.
8307
+ */
7869
8308
  const MONITOR_TASK_TYPES = /* @__PURE__ */ new Set([
7870
- "monitor",
7871
- "monitor_mcp",
8309
+ ...MONITOR_TOOL_TASK_TYPES,
7872
8310
  "local_bash",
7873
8311
  "shell"
7874
8312
  ]);
@@ -12210,6 +12648,44 @@ const TicketResolveResult = Schema$1.Struct({
12210
12648
  */
12211
12649
  description: Schema$1.optional(TrimmedNonEmptyString)
12212
12650
  });
12651
+ /** One link filed alongside a comment: the proof, and what to call it. */
12652
+ const TicketCommentAttachment = Schema$1.Struct({
12653
+ url: TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Publicly reachable URL of the proof to file against the ticket. Local file paths cannot be attached." })),
12654
+ title: Schema$1.optional(TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Label for the link. Defaults to the URL itself." })))
12655
+ });
12656
+ /**
12657
+ * One comment on a ticket, with the links that belong to it.
12658
+ *
12659
+ * Commenting rather than editing: closeout writes what happened, and the
12660
+ * description belongs to whoever filed the ticket. The attachments ride along
12661
+ * because proof without the sentence explaining it is an orphan link.
12662
+ */
12663
+ const TicketCommentInput = Schema$1.Struct({
12664
+ reference: TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "The ticket as it was written: an identifier like MOBILE-12241, or a tracker URL." })),
12665
+ body: TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "The comment, in Markdown, exactly as it should appear on the ticket." })),
12666
+ attachments: Schema$1.optional(Schema$1.Array(TicketCommentAttachment))
12667
+ });
12668
+ /** A link the tracker would not file, and the sentence explaining why. */
12669
+ const TicketCommentAttachmentFailure = Schema$1.Struct({
12670
+ url: TrimmedNonEmptyString,
12671
+ detail: Schema$1.String
12672
+ });
12673
+ const TicketCommentResult = Schema$1.Struct({
12674
+ externalRef: TaskExternalRef,
12675
+ /** Absent when the tracker filed the comment and reported no link to it. */
12676
+ commentUrl: Schema$1.optional(TrimmedNonEmptyString),
12677
+ /** The attachments that landed, in the order they were asked for. */
12678
+ attachedUrls: Schema$1.Array(TrimmedNonEmptyString),
12679
+ /**
12680
+ * The attachments that did not land.
12681
+ *
12682
+ * Reported rather than thrown, because the comment is already on the ticket
12683
+ * by the time a link can fail: a caller told only "it failed" retries the
12684
+ * whole call and files the comment twice. With this it can retry the links
12685
+ * alone, or say in the thread which proof is missing.
12686
+ */
12687
+ failedAttachments: Schema$1.Array(TicketCommentAttachmentFailure)
12688
+ });
12213
12689
  /**
12214
12690
  * Why a reference could not be turned into a ticket.
12215
12691
  *
@@ -12568,251 +13044,6 @@ var ThreadAskFailedError = class extends Schema$1.TaggedErrorClass()("ThreadAskF
12568
13044
  }
12569
13045
  };
12570
13046
  const ThreadAskToolError = Schema$1.Union([ThreadAskNotPermittedError, ThreadAskFailedError]);
12571
- const ThreadWatchEventsInput = Schema$1.Struct({
12572
- threadId: ThreadId.annotate({ description: "The thread to read events from. Any thread on this server, not only this session's own." }),
12573
- afterSequence: Schema$1.optional(NonNegativeInt.annotate({ description: "Exclusive sequence cursor. Events with a higher sequence are returned. Omit to read from the beginning." })),
12574
- limit: Schema$1.optional(NonNegativeInt.check(Schema$1.isLessThanOrEqualTo(200)).annotate({ description: `Maximum events to return (default 50, max 200).` }))
12575
- });
12576
- const ThreadWatchEventsResult = Schema$1.Struct({
12577
- threadId: ThreadId,
12578
- /** Ordered by sequence ascending; resume with the last event's sequence. */
12579
- events: Schema$1.Array(OrchestrationEvent),
12580
- /**
12581
- * The engine's latest persisted sequence at read time. Equal to the last
12582
- * returned event's sequence when the watcher is caught up.
12583
- */
12584
- headSequence: NonNegativeInt,
12585
- /** True when more events for this thread exist past the returned page. */
12586
- hasMore: Schema$1.Boolean
12587
- });
12588
- /** Compact review limits are server-owned, independent of raw event paging. */
12589
- const THREAD_REVIEW_SCAN_LIMIT = 2e3;
12590
- const THREAD_REVIEW_ITEM_MAX_BYTES = 8e3;
12591
- const ThreadWatchReviewInput = Schema$1.Struct({
12592
- threadId: ThreadId,
12593
- afterSequence: Schema$1.optional(NonNegativeInt),
12594
- throughSequence: Schema$1.optional(NonNegativeInt.annotate({ description: "Inclusive fixed review boundary. Omit on the first page to capture the head; reuse the returned boundary on every subsequent page." }))
12595
- });
12596
- const ReviewSourceFields = {
12597
- firstSequence: NonNegativeInt,
12598
- lastSequence: NonNegativeInt,
12599
- truncated: Schema$1.Boolean
12600
- };
12601
- const ThreadReviewItem = Schema$1.Union([Schema$1.Struct({
12602
- ...ReviewSourceFields,
12603
- kind: Schema$1.Literal("message"),
12604
- messageId: Schema$1.String,
12605
- turnId: Schema$1.NullOr(Schema$1.String),
12606
- role: Schema$1.String,
12607
- /** Apply append/replace by message identity across page boundaries. */
12608
- operation: Schema$1.Literals(["append", "replace"]),
12609
- complete: Schema$1.Boolean,
12610
- text: Schema$1.String,
12611
- /**
12612
- * What the message carried besides text.
12613
- *
12614
- * Present only when there was something: a message whose fragments are
12615
- * merged by identity would otherwise lose every trace of an image or a
12616
- * document the builder attached. Bounded by
12617
- * {@link THREAD_REVIEW_ITEM_MAX_ATTACHMENTS} so the item stays trimmable -
12618
- * only its text shrinks under the byte cap, so nothing else may be able to
12619
- * grow without limit.
12620
- */
12621
- attachments: Schema$1.optionalKey(Schema$1.Array(Schema$1.Struct({
12622
- type: Schema$1.String,
12623
- name: Schema$1.String,
12624
- sizeBytes: NonNegativeInt
12625
- })))
12626
- }), Schema$1.Struct({
12627
- ...ReviewSourceFields,
12628
- kind: Schema$1.Literal("event"),
12629
- type: Schema$1.String,
12630
- /** JSON evidence excerpt. Truncated excerpts require targeted raw recovery. */
12631
- payload: Schema$1.String
12632
- })]);
12633
- /**
12634
- * Compressed evidence standing in for the items it replaces.
12635
- *
12636
- * The supervisor reads this instead of paging the range event by event. The
12637
- * sequence bounds are what make that safe: any claim can be verified at source
12638
- * with a targeted `thread_watch_events` read over exactly this range, so the
12639
- * summary is a starting point rather than the only record.
12640
- *
12641
- * `degraded` is set whenever the items came back unsummarized, and names why.
12642
- * Most reasons are failures - unset model, provider error, timeout, empty or
12643
- * unusable output - but `page-under-threshold` is not: it means the page was
12644
- * small enough that summarizing it would have cost more than reading it. The
12645
- * supervisor is never left guessing whether it is reading everything.
12646
- */
12647
- const ThreadReviewSummary = Schema$1.Struct({
12648
- text: Schema$1.String,
12649
- firstSequence: NonNegativeInt,
12650
- lastSequence: NonNegativeInt,
12651
- /** How many builder turns the range covered. */
12652
- turnCount: NonNegativeInt,
12653
- degraded: Schema$1.NullOr(Schema$1.String)
12654
- });
12655
- const ThreadWatchReviewResult = Schema$1.Struct({
12656
- threadId: ThreadId,
12657
- items: Schema$1.Array(ThreadReviewItem),
12658
- throughSequence: NonNegativeInt,
12659
- /** Resume from the source scan cursor, even when items is empty. */
12660
- nextAfterSequence: NonNegativeInt,
12661
- hasMore: Schema$1.Boolean,
12662
- scannedEvents: NonNegativeInt,
12663
- /**
12664
- * Present only when summarization ran. When its `degraded` is null the
12665
- * `items` for the summarized range are omitted and this stands in for them;
12666
- * otherwise the items are intact and this explains why.
12667
- */
12668
- summary: Schema$1.optionalKey(Schema$1.NullOr(ThreadReviewSummary))
12669
- });
12670
- /**
12671
- * How a review ended, from the supervisor's own account of it.
12672
- *
12673
- * Three states rather than a boolean because they are acted on differently: a
12674
- * summary that arrived is evidence, a fallback is a working review that cost
12675
- * full context, and a failure is a misconfiguration worth fixing. The fourth
12676
- * state - a read no report ever answered - is not sayable here: only the
12677
- * server can observe it, through `ThreadEvidenceActivityOutcome`.
12678
- */
12679
- const ThreadEvidenceReportOutcome = Schema$1.Literals([
12680
- "summarized",
12681
- "fallback",
12682
- "failed"
12683
- ]);
12684
- /** Bounded so one report cannot dominate a thread's activity storage. */
12685
- const THREAD_EVIDENCE_REPORT_MAX_CHARS = 8e3;
12686
- const ThreadEvidenceReportInput = Schema$1.Struct({
12687
- /** The watched thread the summary covers, not the reporting thread. */
12688
- threadId: ThreadId,
12689
- firstSequence: NonNegativeInt,
12690
- lastSequence: NonNegativeInt,
12691
- /**
12692
- * The fixed review boundary the pages were read against.
12693
- *
12694
- * Identity, not description: the evidence-read rows are keyed on it, so a
12695
- * report that guessed from its own last sequence would look for a baseline
12696
- * that never existed whenever the range ended on a filtered event.
12697
- */
12698
- throughSequence: NonNegativeInt,
12699
- outcome: ThreadEvidenceReportOutcome,
12700
- /** The summarizer's exact text. Required when the outcome is summarized. */
12701
- summary: Schema$1.optional(Schema$1.String),
12702
- /** The model the subagent ran on, when one was spawned. */
12703
- model: Schema$1.optional(Schema$1.String),
12704
- /** Why the summary is missing or degraded. Required unless summarized. */
12705
- reason: Schema$1.optional(Schema$1.String)
12706
- });
12707
- /**
12708
- * The supervisor's own context around the read, correlated by the server.
12709
- *
12710
- * Self-reported numbers would be worth little, so these are read from the
12711
- * reporting thread's projected context-window rows: `before` is what was
12712
- * recorded when the evidence read happened, `after` what is recorded when the
12713
- * report lands. `delta` can be negative when a compaction ran in between, and
12714
- * is null whenever no read-time baseline exists.
12715
- */
12716
- const ThreadEvidenceReportContext = Schema$1.Struct({
12717
- /**
12718
- * Null when no reading was recorded at read time.
12719
- *
12720
- * Never the report-time value standing in for itself: a delta of zero read
12721
- * as measurement would be a lie about a review that cost context.
12722
- */
12723
- usedTokensBefore: Schema$1.NullOr(NonNegativeInt),
12724
- usedTokensAfter: NonNegativeInt,
12725
- /** Null exactly when `usedTokensBefore` is. */
12726
- deltaTokens: Schema$1.NullOr(Schema$1.Number),
12727
- maxTokens: Schema$1.NullOr(NonNegativeInt),
12728
- capacityPercent: Schema$1.NullOr(Schema$1.Number)
12729
- });
12730
- /**
12731
- * How the recorded row ended, which is the supervisor's three outcomes plus the
12732
- * one only the server can state: the review closed with no report at all.
12733
- *
12734
- * `unreported` is never accepted from a tool call. The server writes it when a
12735
- * supervisor turn ends over an evidence read that nothing answered, so the
12736
- * absence survives outside a live client that could infer it from two rows.
12737
- */
12738
- const ThreadEvidenceActivityOutcome = Schema$1.Literals([
12739
- "summarized",
12740
- "fallback",
12741
- "failed",
12742
- "unreported"
12743
- ]);
12744
- /**
12745
- * Server clock around one review, never the supervisor's own account of it.
12746
- *
12747
- * `readAt` is when the evidence-read row was recorded and `closedAt` when the
12748
- * row that closes the review was: the report for a reported review, the
12749
- * completed supervisor turn for an unreported one. A supervisor turn that
12750
- * failed or was interrupted closes nothing, so its read keeps no timing until
12751
- * some later turn reports on it.
12752
- */
12753
- const ThreadEvidenceReportTiming = Schema$1.Struct({
12754
- readAt: IsoDateTime,
12755
- closedAt: IsoDateTime,
12756
- durationMs: NonNegativeInt
12757
- });
12758
- /** Payload of the single `fusion.evidence.summary` activity a report records. */
12759
- const ThreadEvidenceReportActivity = Schema$1.Struct({
12760
- watchedThreadId: ThreadId,
12761
- firstSequence: NonNegativeInt,
12762
- lastSequence: NonNegativeInt,
12763
- throughSequence: NonNegativeInt,
12764
- outcome: ThreadEvidenceActivityOutcome,
12765
- summary: Schema$1.NullOr(Schema$1.String),
12766
- /** True when a snapshot dropped the text of a superseded report. */
12767
- summaryDropped: Schema$1.optionalKey(Schema$1.Boolean),
12768
- model: Schema$1.NullOr(Schema$1.String),
12769
- reason: Schema$1.NullOr(Schema$1.String),
12770
- /** Null when no context-window row exists yet for the reporting thread. */
12771
- context: Schema$1.NullOr(ThreadEvidenceReportContext),
12772
- /**
12773
- * Null when the evidence-read row behind this review is no longer readable,
12774
- * and absent on rows stored before the server measured a review at all.
12775
- */
12776
- timing: Schema$1.optionalKey(Schema$1.NullOr(ThreadEvidenceReportTiming))
12777
- });
12778
- const ThreadEvidenceReportResult = Schema$1.Struct({
12779
- recorded: Schema$1.Boolean,
12780
- context: Schema$1.NullOr(ThreadEvidenceReportContext)
12781
- });
12782
- var WatchToolUnavailableError = class extends Schema$1.TaggedErrorClass()("WatchToolUnavailableError", {
12783
- capability: Schema$1.Literal("watch"),
12784
- environmentId: EnvironmentId,
12785
- threadId: ThreadId,
12786
- providerSessionId: TrimmedNonEmptyString,
12787
- providerInstanceId: ProviderInstanceId
12788
- }) {
12789
- get message() {
12790
- return `MCP credential does not grant the ${this.capability} capability.`;
12791
- }
12792
- };
12793
- /**
12794
- * The capability is present but the requested thread is not in the granted
12795
- * set. Separate from {@link WatchToolUnavailableError} so a watcher can tell
12796
- * "you cannot watch at all" apart from "not this thread".
12797
- */
12798
- var ThreadWatchNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadWatchNotPermittedError", { threadId: ThreadId }) {
12799
- get message() {
12800
- return `This session was not granted watch access to thread ${this.threadId}.`;
12801
- }
12802
- };
12803
- var ThreadWatchFailedError = class extends Schema$1.TaggedErrorClass()("ThreadWatchFailedError", {
12804
- threadId: ThreadId,
12805
- detail: Schema$1.String
12806
- }) {
12807
- get message() {
12808
- return `Reading events for thread ${this.threadId} failed: ${this.detail}`;
12809
- }
12810
- };
12811
- const ThreadWatchToolError = Schema$1.Union([
12812
- WatchToolUnavailableError,
12813
- ThreadWatchNotPermittedError,
12814
- ThreadWatchFailedError
12815
- ]);
12816
13047
  //#endregion
12817
13048
  //#region ../../packages/contracts/src/threadControl.ts
12818
13049
  /**
@@ -14412,10 +14643,19 @@ const UserInputRankingInput = Schema$1.Struct({
14412
14643
  requestId: ApprovalRequestId,
14413
14644
  questionId: Schema$1.String
14414
14645
  });
14415
- const UserInputRankingResult = Schema$1.Struct({ ranks: Schema$1.Array(Schema$1.Struct({
14416
- optionIndex: Schema$1.Int,
14417
- rank: Schema$1.Int
14418
- })) });
14646
+ /**
14647
+ * `probability` is the option's share of the ranker's distribution and
14648
+ * `confidence` how concentrated that distribution is. Both come from the
14649
+ * TypeSafe answer; neither is invented when the answer carries none.
14650
+ */
14651
+ const UserInputRankingResult = Schema$1.Struct({
14652
+ confidence: Schema$1.Number,
14653
+ ranks: Schema$1.Array(Schema$1.Struct({
14654
+ optionIndex: Schema$1.Int,
14655
+ rank: Schema$1.Int,
14656
+ probability: Schema$1.Number
14657
+ }))
14658
+ });
14419
14659
  var UserInputRankingError = class extends Schema$1.TaggedErrorClass()("UserInputRankingError", { detail: Schema$1.String }) {};
14420
14660
  //#endregion
14421
14661
  //#region ../../packages/contracts/src/providerSetup.ts
@@ -14928,6 +15168,7 @@ const WS_METHODS = {
14928
15168
  hubSetSyncMode: "hub.setSyncMode",
14929
15169
  hubSetShareMode: "hub.setShareMode",
14930
15170
  hubMintToken: "hub.mintToken",
15171
+ hubAnalyticsQuery: "hub.analyticsQuery",
14931
15172
  settingsSyncNow: "settingsSync.now",
14932
15173
  storageCleanupPreview: "storageCleanup.preview",
14933
15174
  storageCleanupSweep: "storageCleanup.sweep",
@@ -15141,6 +15382,19 @@ const WsWorktreeCleanupSweepRpc = Rpc.make(WS_METHODS.worktreeCleanupSweep, {
15141
15382
  success: WorktreeCleanupPreview,
15142
15383
  error: Schema$1.Union([EnvironmentAuthorizationError])
15143
15384
  });
15385
+ /**
15386
+ * The account's own analytics facts, read through this server.
15387
+ *
15388
+ * The hub credential belongs to the environment, not to a browser tab, so the
15389
+ * client never holds it: it asks its own server, which is already the only
15390
+ * thing that can authenticate against the hub. Read-only, and the query is the
15391
+ * hub's own contract, so a filter the hub rejects is rejected here too.
15392
+ */
15393
+ const WsHubAnalyticsQueryRpc = Rpc.make(WS_METHODS.hubAnalyticsQuery, {
15394
+ payload: HubAnalyticsQuery,
15395
+ success: HubAnalyticsPage,
15396
+ error: HubRpcError
15397
+ });
15144
15398
  const WsHubGetSyncStatusRpc = Rpc.make(WS_METHODS.hubGetSyncStatus, {
15145
15399
  payload: Schema$1.Struct({}),
15146
15400
  success: AgentSyncStatus,
@@ -16092,7 +16346,7 @@ const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, {
16092
16346
  error: Schema$1.Union([AuthAccessStreamError, EnvironmentAuthorizationError]),
16093
16347
  stream: true
16094
16348
  });
16095
- const WsRpcGroup = RpcGroup.make(WsDeviceGetStateRpc, WsDeviceListRpc, WsDeviceConfigureRpc, WsDeviceOpenRpc, WsDeviceCloseRpc, WsDeviceDetailRpc, WsDeviceActionRpc, WsSubscribeDevicesRpc, WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerProviderAuthStartRpc, WsServerProviderAuthCompleteRpc, WsServerProviderAuthCancelRpc, WsServerProviderAuthLogoutRpc, WsServerProviderAuthSubscribeRpc, WsServerProviderInstallStartRpc, WsServerProviderInstallCancelRpc, WsServerProviderInstallRemoveRpc, WsServerProviderInstallSubscribeRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerGetFusionDefaultPromptsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetProviderUsageRpc, WsServerGetHostResourcesRpc, WsServerGetResourceTelemetryRpc, WsServerGetResourceTelemetryHistoryRpc, WsServerRetryResourceTelemetryRpc, WsSubscribeResourceTelemetryRpc, WsThreadHandoffRpc, WsUserInputRankRpc, WsBtwAskRpc, WsBtwCancelRpc, WsServerGetUsageSummaryRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsStackRpc, WsPullRequestsActivityRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchContentsRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsComputerUseConnectRpc, WsComputerUseListRpc, WsComputerUseControlsRpc, WsComputerUseControlRpc, WsComputerUsePreviewRpc, WsComputerUseRespondRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsTasksStartThreadRpc, WsFeedListRpc, WsFeedRefreshRpc, WsFeedSourcesListRpc, WsFeedSourcesUpsertRpc, WsFeedSourcesDeleteRpc, WsFeedMarkReadRpc, WsFeedCleanupRpc, WsSubscribeTasksRpc, WsStorageCleanupPreviewRpc, WsStorageCleanupSweepRpc, WsWorktreeCleanupPreviewRpc, WsWorktreeCleanupSweepRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSettingsSyncNowRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsAssetsReadRpc, WsAssetsSaveRpc, WsAssetsDeleteRpc, WsAssetsCreateLocalRpc, WsAssetsRemoveLocalRpc, WsSkillRegistrySearchRpc, WsSkillRegistryFetchRpc, WsMcpListRpc, WsMcpSaveRpc, WsMcpRemoveRpc, WsMcpSetSecretRpc, WsMcpOAuthBeginRpc, WsMcpOAuthDisconnectRpc, WsTrackerStatusRpc, WsTrackerSetApiKeyRpc, WsSupervisorSecretStatusRpc, WsSupervisorSetApiKeyRpc, WsSupervisorClearApiKeyRpc, WsTrackerClearApiKeyRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetWorkflowScriptRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationSearchThreadsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
16349
+ const WsRpcGroup = RpcGroup.make(WsDeviceGetStateRpc, WsDeviceListRpc, WsDeviceConfigureRpc, WsDeviceOpenRpc, WsDeviceCloseRpc, WsDeviceDetailRpc, WsDeviceActionRpc, WsSubscribeDevicesRpc, WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerProviderAuthStartRpc, WsServerProviderAuthCompleteRpc, WsServerProviderAuthCancelRpc, WsServerProviderAuthLogoutRpc, WsServerProviderAuthSubscribeRpc, WsServerProviderInstallStartRpc, WsServerProviderInstallCancelRpc, WsServerProviderInstallRemoveRpc, WsServerProviderInstallSubscribeRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerGetFusionDefaultPromptsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetProviderUsageRpc, WsServerGetHostResourcesRpc, WsServerGetResourceTelemetryRpc, WsServerGetResourceTelemetryHistoryRpc, WsServerRetryResourceTelemetryRpc, WsSubscribeResourceTelemetryRpc, WsThreadHandoffRpc, WsUserInputRankRpc, WsBtwAskRpc, WsBtwCancelRpc, WsServerGetUsageSummaryRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsStackRpc, WsPullRequestsActivityRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchContentsRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsComputerUseConnectRpc, WsComputerUseListRpc, WsComputerUseControlsRpc, WsComputerUseControlRpc, WsComputerUsePreviewRpc, WsComputerUseRespondRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsTasksStartThreadRpc, WsFeedListRpc, WsFeedRefreshRpc, WsFeedSourcesListRpc, WsFeedSourcesUpsertRpc, WsFeedSourcesDeleteRpc, WsFeedMarkReadRpc, WsFeedCleanupRpc, WsSubscribeTasksRpc, WsStorageCleanupPreviewRpc, WsStorageCleanupSweepRpc, WsWorktreeCleanupPreviewRpc, WsWorktreeCleanupSweepRpc, WsHubAnalyticsQueryRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSettingsSyncNowRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsAssetsReadRpc, WsAssetsSaveRpc, WsAssetsDeleteRpc, WsAssetsCreateLocalRpc, WsAssetsRemoveLocalRpc, WsSkillRegistrySearchRpc, WsSkillRegistryFetchRpc, WsMcpListRpc, WsMcpSaveRpc, WsMcpRemoveRpc, WsMcpSetSecretRpc, WsMcpOAuthBeginRpc, WsMcpOAuthDisconnectRpc, WsTrackerStatusRpc, WsTrackerSetApiKeyRpc, WsSupervisorSecretStatusRpc, WsSupervisorSetApiKeyRpc, WsSupervisorClearApiKeyRpc, WsTrackerClearApiKeyRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetWorkflowScriptRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationSearchThreadsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
16096
16350
  //#endregion
16097
16351
  //#region ../../packages/shared/src/oauthScope.ts
16098
16352
  const OAUTH_SCOPE_TOKEN = /^[\u0021\u0023-\u005b\u005d-\u007e]+$/u;
@@ -16256,7 +16510,7 @@ const make$120 = Effect.gen(function* () {
16256
16510
  };
16257
16511
  return EnvironmentAuthPolicy.of({ getDescriptor: () => Effect.succeed(descriptor).pipe(Effect.withSpan("EnvironmentAuthPolicy.getDescriptor")) });
16258
16512
  });
16259
- const layer$120 = Layer.effect(EnvironmentAuthPolicy, make$120);
16513
+ const layer$121 = Layer.effect(EnvironmentAuthPolicy, make$120);
16260
16514
  //#endregion
16261
16515
  //#region src/persistence/Errors.ts
16262
16516
  function summarizeSchemaIssue(issue) {
@@ -16571,7 +16825,7 @@ const make$119 = Effect.gen(function* () {
16571
16825
  setLastConnectedAt
16572
16826
  };
16573
16827
  });
16574
- const layer$119 = Layer.effect(AuthSessionRepository, make$119);
16828
+ const layer$120 = Layer.effect(AuthSessionRepository, make$119);
16575
16829
  //#endregion
16576
16830
  //#region src/auth/ServerSecretStore.ts
16577
16831
  const secretStoreErrorContext = {
@@ -16717,7 +16971,7 @@ const make$118 = Effect.gen(function* () {
16717
16971
  const config = yield* ServerConfig$1;
16718
16972
  return yield* makeForDirectory(config.secretsDir);
16719
16973
  });
16720
- const layer$118 = Layer.effect(ServerSecretStore, make$118);
16974
+ const layer$119 = Layer.effect(ServerSecretStore, make$118);
16721
16975
  //#endregion
16722
16976
  //#region src/auth/SessionStore.ts
16723
16977
  var MalformedSessionTokenError = class extends Schema$1.TaggedErrorClass()("MalformedSessionTokenError", {}) {
@@ -17270,7 +17524,7 @@ const make$117 = Effect.gen(function* () {
17270
17524
  markDisconnected
17271
17525
  });
17272
17526
  });
17273
- const layer$117 = Layer.effect(SessionStore, make$117).pipe(Layer.provideMerge(layer$119));
17527
+ const layer$118 = Layer.effect(SessionStore, make$117).pipe(Layer.provideMerge(layer$120));
17274
17528
  //#endregion
17275
17529
  //#region src/persistence/AuthPairingLinks.ts
17276
17530
  const AuthPairingLinkRecord = Schema$1.Struct({
@@ -17466,7 +17720,7 @@ const make$116 = Effect.gen(function* () {
17466
17720
  getByCredential
17467
17721
  };
17468
17722
  });
17469
- const layer$116 = Layer.effect(AuthPairingLinkRepository, make$116);
17723
+ const layer$117 = Layer.effect(AuthPairingLinkRepository, make$116);
17470
17724
  //#endregion
17471
17725
  //#region src/auth/PairingGrantStore.ts
17472
17726
  var UnknownBootstrapCredentialError = class extends Schema$1.TaggedErrorClass()("UnknownBootstrapCredentialError", {}) {
@@ -17759,7 +18013,7 @@ const make$115 = Effect.gen(function* () {
17759
18013
  consume
17760
18014
  });
17761
18015
  });
17762
- const layer$115 = Layer.effect(PairingGrantStore, make$115).pipe(Layer.provideMerge(layer$116));
18016
+ const layer$116 = Layer.effect(PairingGrantStore, make$115).pipe(Layer.provideMerge(layer$117));
17763
18017
  //#endregion
17764
18018
  //#region src/persistence/DatabaseSnapshot.ts
17765
18019
  /**
@@ -17901,6 +18155,21 @@ var _060_ThreadPairTriageCursor_default = Effect.gen(function* () {
17901
18155
  yield* (yield* SqlClient.SqlClient)`ALTER TABLE thread_pairs ADD COLUMN triaged_from_sequence INTEGER`;
17902
18156
  });
17903
18157
  //#endregion
18158
+ //#region src/persistence/Migrations/061_AnalyticsFactOutbox.ts
18159
+ var _061_AnalyticsFactOutbox_default = Effect.gen(function* () {
18160
+ const sql = yield* SqlClient.SqlClient;
18161
+ yield* sql`CREATE TABLE analytics_fact_outbox (
18162
+ event_id TEXT PRIMARY KEY, kind TEXT NOT NULL, version INTEGER NOT NULL,
18163
+ occurred_at TEXT NOT NULL, payload TEXT NOT NULL,
18164
+ pending INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL
18165
+ )`;
18166
+ yield* sql`CREATE INDEX analytics_fact_outbox_pending
18167
+ ON analytics_fact_outbox(pending, created_at, event_id)`;
18168
+ yield* sql`CREATE TABLE analytics_fact_cursors (
18169
+ source TEXT PRIMARY KEY, after_sequence INTEGER NOT NULL
18170
+ )`;
18171
+ });
18172
+ //#endregion
17904
18173
  //#region src/persistence/Migrations/001_OrchestrationEvents.ts
17905
18174
  var _001_OrchestrationEvents_default = Effect.gen(function* () {
17906
18175
  const sql = yield* SqlClient.SqlClient;
@@ -20082,6 +20351,11 @@ const migrationEntries = [
20082
20351
  60,
20083
20352
  "ThreadPairTriageCursor",
20084
20353
  _060_ThreadPairTriageCursor_default
20354
+ ],
20355
+ [
20356
+ 61,
20357
+ "AnalyticsFactOutbox",
20358
+ _061_AnalyticsFactOutbox_default
20085
20359
  ]
20086
20360
  ];
20087
20361
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -20429,22 +20703,22 @@ function classifyToolCategory(input) {
20429
20703
  if (normalized.includes("image")) return "image_view";
20430
20704
  return "tool";
20431
20705
  }
20432
- function asRecord$10(value) {
20706
+ function asRecord$12(value) {
20433
20707
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
20434
20708
  }
20435
20709
  /** Classify from a runtime item payload's `data` (`{ toolName, input }`). */
20436
20710
  function classifyToolCategoryFromToolData(data) {
20437
- const record = asRecord$10(data);
20711
+ const record = asRecord$12(data);
20438
20712
  const toolName = record?.toolName;
20439
20713
  if (typeof toolName !== "string" || toolName.trim().length === 0) return;
20440
20714
  return classifyToolCategory({
20441
20715
  toolName,
20442
- toolInput: asRecord$10(record?.input)
20716
+ toolInput: asRecord$12(record?.input)
20443
20717
  });
20444
20718
  }
20445
20719
  //#endregion
20446
20720
  //#region src/orchestration/ActivityPayloadProjection.ts
20447
- function asRecord$9(value) {
20721
+ function asRecord$11(value) {
20448
20722
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
20449
20723
  }
20450
20724
  function asTrimmedString$2(value) {
@@ -20474,7 +20748,7 @@ function compactMcpRecord(record) {
20474
20748
  const entries = Object.entries(record);
20475
20749
  const projected = {};
20476
20750
  for (const [key, value] of entries.slice(0, MCP_TOOL_ACTIVITY_FIELD_LIMIT)) {
20477
- const nestedItem = key === "item" ? asRecord$9(value) : null;
20751
+ const nestedItem = key === "item" ? asRecord$11(value) : null;
20478
20752
  projected[key] = nestedItem === null ? compactMcpField(value) : compactMcpRecord(nestedItem);
20479
20753
  }
20480
20754
  if (entries.length > MCP_TOOL_ACTIVITY_FIELD_LIMIT) projected.truncatedFields = entries.length - MCP_TOOL_ACTIVITY_FIELD_LIMIT;
@@ -20500,7 +20774,7 @@ function collectChangedFiles(value, target, seen, depth) {
20500
20774
  }
20501
20775
  return;
20502
20776
  }
20503
- const record = asRecord$9(value);
20777
+ const record = asRecord$11(value);
20504
20778
  if (!record) return;
20505
20779
  pushChangedFile(target, seen, record.path);
20506
20780
  pushChangedFile(target, seen, record.filePath);
@@ -20526,13 +20800,13 @@ function collectChangedFiles(value, target, seen, depth) {
20526
20800
  }
20527
20801
  }
20528
20802
  function projectCommandData(data) {
20529
- const item = asRecord$9(data.item);
20803
+ const item = asRecord$11(data.item);
20530
20804
  if (!item) return;
20531
20805
  const projectedItem = {};
20532
20806
  if ("command" in item) projectedItem.command = item.command;
20533
- const input = asRecord$9(item.input);
20807
+ const input = asRecord$11(item.input);
20534
20808
  if (input && "command" in input) projectedItem.input = { command: input.command };
20535
- const result = asRecord$9(item.result);
20809
+ const result = asRecord$11(item.result);
20536
20810
  if (result && "command" in result) projectedItem.result = { command: result.command };
20537
20811
  return Object.keys(projectedItem).length > 0 ? projectedItem : void 0;
20538
20812
  }
@@ -20556,7 +20830,7 @@ function summarizeToolTextOutput(value) {
20556
20830
  return meaningfulLineCount > 1 ? `${meaningfulLineCount.toLocaleString()} lines` : null;
20557
20831
  }
20558
20832
  function projectRawOutput(value) {
20559
- const rawOutput = asRecord$9(value);
20833
+ const rawOutput = asRecord$11(value);
20560
20834
  if (!rawOutput) return;
20561
20835
  if (typeof rawOutput.totalFiles === "number" && Number.isFinite(rawOutput.totalFiles)) return {
20562
20836
  totalFiles: rawOutput.totalFiles,
@@ -20579,8 +20853,8 @@ function projectRawOutput(value) {
20579
20853
  * tool outputs do not accumulate twice in the event store and activity projection.
20580
20854
  */
20581
20855
  function projectActivityPayload(activity) {
20582
- const payload = asRecord$9(activity.payload);
20583
- const data = asRecord$9(payload?.data);
20856
+ const payload = asRecord$11(activity.payload);
20857
+ const data = asRecord$11(payload?.data);
20584
20858
  if (!payload || !data) return activity;
20585
20859
  if (payload.itemType === "mcp_tool_call") {
20586
20860
  const projectedMcpData = projectMcpToolCallData(data);
@@ -20597,7 +20871,7 @@ function projectActivityPayload(activity) {
20597
20871
  const item = projectCommandData(data);
20598
20872
  if (item) projectedData.item = item;
20599
20873
  if ("command" in data) projectedData.command = data.command;
20600
- const input = asRecord$9(data.input);
20874
+ const input = asRecord$11(data.input);
20601
20875
  if (input) {
20602
20876
  const projectedInput = {};
20603
20877
  if ("command" in input) projectedInput.command = input.command;
@@ -20637,7 +20911,7 @@ function projectActivityPayload(activity) {
20637
20911
  */
20638
20912
  function isResolvableContextWindowActivity(activity) {
20639
20913
  if (activity.kind !== "context-window.updated") return false;
20640
- const usedTokens = asRecord$9(activity.payload)?.usedTokens;
20914
+ const usedTokens = asRecord$11(activity.payload)?.usedTokens;
20641
20915
  return typeof usedTokens === "number" && Number.isFinite(usedTokens) && usedTokens >= 0;
20642
20916
  }
20643
20917
  /**
@@ -20653,7 +20927,7 @@ function isResolvableContextWindowActivity(activity) {
20653
20927
  * client.
20654
20928
  */
20655
20929
  function withoutContextWindowBreakdown$1(activity) {
20656
- const payload = asRecord$9(activity.payload);
20930
+ const payload = asRecord$11(activity.payload);
20657
20931
  if (!payload || payload.breakdown === void 0) return activity;
20658
20932
  const { breakdown: _breakdown, ...rest } = payload;
20659
20933
  return {
@@ -20668,7 +20942,7 @@ function dropStaleContextWindowActivities(activities) {
20668
20942
  const retainedIndexes = new Set(latestIndexByTurn.values());
20669
20943
  let breakdownIndex = null;
20670
20944
  for (const index of retainedIndexes) {
20671
- if (asRecord$9(activities[index].payload)?.breakdown === void 0) continue;
20945
+ if (asRecord$11(activities[index].payload)?.breakdown === void 0) continue;
20672
20946
  if (breakdownIndex === null || index > breakdownIndex) breakdownIndex = index;
20673
20947
  }
20674
20948
  return activities.flatMap((activity, index) => {
@@ -20678,12 +20952,12 @@ function dropStaleContextWindowActivities(activities) {
20678
20952
  });
20679
20953
  }
20680
20954
  function toolLifecycleIdentity(activity) {
20681
- return asTrimmedString$2(asRecord$9(asRecord$9(activity.payload)?.data)?.toolCallId);
20955
+ return asTrimmedString$2(asRecord$11(asRecord$11(activity.payload)?.data)?.toolCallId);
20682
20956
  }
20683
20957
  /** Clients retain fields missing from a completion when folding an update. */
20684
20958
  function completionPreservesUpdate(update, completion) {
20685
- const updatePayload = asRecord$9(update.payload);
20686
- const completionPayload = asRecord$9(completion.payload);
20959
+ const updatePayload = asRecord$11(update.payload);
20960
+ const completionPayload = asRecord$11(completion.payload);
20687
20961
  if (!updatePayload || !completionPayload) return false;
20688
20962
  return Object.entries(updatePayload).every(([key, value]) => key === "status" || NodeUtil.isDeepStrictEqual(value, completionPayload[key]));
20689
20963
  }
@@ -20722,13 +20996,13 @@ function dropSupersededEvidenceSummaries(activities) {
20722
20996
  for (let index = 0; index < activities.length; index += 1) {
20723
20997
  const activity = activities[index];
20724
20998
  if (activity.kind !== "fusion.evidence.summary") continue;
20725
- const watchedThreadId = asRecord$9(activity.payload)?.watchedThreadId;
20999
+ const watchedThreadId = asRecord$11(activity.payload)?.watchedThreadId;
20726
21000
  if (typeof watchedThreadId === "string") latestIndexByWatched.set(watchedThreadId, index);
20727
21001
  }
20728
21002
  if (latestIndexByWatched.size === 0) return activities;
20729
21003
  return activities.map((activity, index) => {
20730
21004
  if (activity.kind !== "fusion.evidence.summary") return activity;
20731
- const payload = asRecord$9(activity.payload);
21005
+ const payload = asRecord$11(activity.payload);
20732
21006
  const watchedThreadId = payload?.watchedThreadId;
20733
21007
  if (!payload || typeof watchedThreadId !== "string" || latestIndexByWatched.get(watchedThreadId) === index || payload.summary === null || payload.summary === void 0) return activity;
20734
21008
  return {
@@ -21339,9 +21613,9 @@ const make$114 = Effect.gen(function* () {
21339
21613
  issueStartupPairingUrl
21340
21614
  });
21341
21615
  });
21342
- const layer$114 = Layer.effect(EnvironmentAuth, make$114).pipe(Layer.provideMerge(layer$115), Layer.provideMerge(layer$117), Layer.provideMerge(layer$120));
21343
- const storageLayer = Layer.mergeAll(layer$118, layerConfig);
21344
- const runtimeLayer = layer$114.pipe(Layer.provideMerge(storageLayer));
21616
+ const layer$115 = Layer.effect(EnvironmentAuth, make$114).pipe(Layer.provideMerge(layer$116), Layer.provideMerge(layer$118), Layer.provideMerge(layer$121));
21617
+ const storageLayer = Layer.mergeAll(layer$119, layerConfig);
21618
+ const runtimeLayer = layer$115.pipe(Layer.provideMerge(storageLayer));
21345
21619
  //#endregion
21346
21620
  //#region ../../packages/shared/src/Struct.ts
21347
21621
  function deepMerge(current, patch) {
@@ -23189,7 +23463,7 @@ const runWithEnvironmentAuth = (flags, run, options) => Effect.gen(function* ()
23189
23463
  }
23190
23464
  return yield* Effect.gen(function* () {
23191
23465
  return yield* run(yield* EnvironmentAuth);
23192
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$121(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
23466
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$122(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
23193
23467
  });
23194
23468
  const ttlFlag = Flag.string("ttl").pipe(Flag.withSchema(DurationFromString), Flag.withDescription("TTL, for example `5m`, `1h`, `30d`, or `15 minutes`."), Flag.optional);
23195
23469
  const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Emit JSON instead of human-readable output."), Flag.withDefault(false));
@@ -23349,7 +23623,7 @@ const make$113 = Effect.fn("HubAuthThrottle.make")(function* (limits = DEFAULT_L
23349
23623
  })
23350
23624
  });
23351
23625
  });
23352
- const layer$113 = Layer.effect(HubAuthThrottle, make$113());
23626
+ const layer$114 = Layer.effect(HubAuthThrottle, make$113());
23353
23627
  //#endregion
23354
23628
  //#region src/hub/HubAuth.ts
23355
23629
  /**
@@ -24122,6 +24396,88 @@ const makeTaskRepository = Effect.gen(function* () {
24122
24396
  });
24123
24397
  const TaskRepositoryLive = Layer.effect(TaskRepository, makeTaskRepository);
24124
24398
  //#endregion
24399
+ //#region src/hub/HubClientCors.ts
24400
+ const MAX_ORIGIN_CHARS = 2048;
24401
+ const DESKTOP_RENDERER_ORIGINS$1 = /* @__PURE__ */ new Set(["p4code://app", "p4code-dev://app"]);
24402
+ const MAX_PREFLIGHT_HEADER_CHARS = 128;
24403
+ const ALLOWED_HEADERS = /* @__PURE__ */ new Set([
24404
+ "authorization",
24405
+ "content-type",
24406
+ "accept"
24407
+ ]);
24408
+ const CLIENT_ROUTES = {
24409
+ "/v1/client-enrollments/exchange": ["POST"],
24410
+ "/v1/clients/me": ["GET", "DELETE"],
24411
+ "/v1/clients/me/events": ["GET"],
24412
+ "/v1/clients/me/socket-ticket": ["POST"],
24413
+ "/v1/clients/me/asset-grant": ["POST"],
24414
+ "/v1/clients/me/notifications": [
24415
+ "GET",
24416
+ "PUT",
24417
+ "DELETE"
24418
+ ],
24419
+ "/v1/clients/me/notifications/preferences": ["PUT"],
24420
+ "/v1/clients/me/live-activities": ["PUT", "DELETE"]
24421
+ };
24422
+ const routeMethods = (path) => Object.hasOwn(CLIENT_ROUTES, path) ? CLIENT_ROUTES[path] : void 0;
24423
+ const denied$2 = () => HttpServerResponse.jsonUnsafe({ error: "invalid_client_origin" }, {
24424
+ status: 403,
24425
+ headers: {
24426
+ "cache-control": "no-store",
24427
+ vary: "Origin"
24428
+ }
24429
+ });
24430
+ /** Arbitrary self-hosted browser clients are allowed; authentication uses explicit credentials, never cookies. */
24431
+ function hubClientCorsOrigin(value) {
24432
+ if (DESKTOP_RENDERER_ORIGINS$1.has(value)) return value;
24433
+ if (value.length > MAX_ORIGIN_CHARS) return void 0;
24434
+ try {
24435
+ const url = new URL(value);
24436
+ if (url.protocol !== "https:" && url.protocol !== "http:" || value !== url.origin || url.username || url.password) return void 0;
24437
+ return url.origin;
24438
+ } catch {
24439
+ return;
24440
+ }
24441
+ }
24442
+ /** Wrap only client-authenticated routes, after their generic failure mapping. */
24443
+ const withHubClientCors = (operation) => Effect.gen(function* () {
24444
+ const request = yield* HttpServerRequest.HttpServerRequest;
24445
+ const path = request.url.split("?")[0];
24446
+ if (!routeMethods(path)?.includes(request.method)) return denied$2();
24447
+ const raw = request.headers.origin;
24448
+ if (raw === void 0) return yield* operation;
24449
+ const origin = hubClientCorsOrigin(raw);
24450
+ if (!origin) return denied$2();
24451
+ const response = yield* operation;
24452
+ return response.pipe(HttpServerResponse.setHeaders({
24453
+ "access-control-allow-origin": origin,
24454
+ "access-control-expose-headers": "retry-after",
24455
+ "cache-control": "no-store",
24456
+ vary: response.headers.vary ? `${response.headers.vary}, Origin` : "Origin"
24457
+ }));
24458
+ });
24459
+ const hubClientPreflight = Effect.gen(function* () {
24460
+ const request = yield* HttpServerRequest.HttpServerRequest;
24461
+ const origin = hubClientCorsOrigin(request.headers.origin ?? "");
24462
+ const methods = routeMethods(request.url);
24463
+ const method = request.headers["access-control-request-method"];
24464
+ const raw = request.headers["access-control-request-headers"] ?? "";
24465
+ if (raw.length > MAX_PREFLIGHT_HEADER_CHARS) return denied$2();
24466
+ const headers = raw === "" ? [] : raw.split(",").map((value) => value.trim().toLowerCase());
24467
+ if (!origin || !methods || !method || !methods.includes(method) || raw.length > MAX_PREFLIGHT_HEADER_CHARS || headers.length > ALLOWED_HEADERS.size || new Set(headers).size !== headers.length || headers.some((value) => !ALLOWED_HEADERS.has(value))) return denied$2();
24468
+ return HttpServerResponse.empty({
24469
+ status: 204,
24470
+ headers: {
24471
+ "access-control-allow-origin": origin,
24472
+ "access-control-allow-methods": methods.join(", "),
24473
+ "access-control-allow-headers": "authorization, content-type, accept",
24474
+ "cache-control": "no-store",
24475
+ vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
24476
+ }
24477
+ });
24478
+ });
24479
+ const layer$113 = Layer.mergeAll(Layer.empty, ...Object.keys(CLIENT_ROUTES).map((path) => HttpRouter.add("OPTIONS", path, hubClientPreflight)));
24480
+ //#endregion
24125
24481
  //#region src/hub/HubClientRevocations.ts
24126
24482
  var HubClientRevocations = class extends Context.Service()("@p4code/cli/hub/HubClientRevocations") {};
24127
24483
  /** Coalesced invalidations carry no identity or credential; consumers recheck durable state. */
@@ -24282,88 +24638,6 @@ const signOutHubClient = Effect.fn("HubClients.signOut")(function* (credential)
24282
24638
  WHERE id = ${principal.clientId} AND credential_hash = ${hashHubToken(credential)}`;
24283
24639
  })).pipe(Effect.ensuring(notifications.changed));
24284
24640
  });
24285
- //#endregion
24286
- //#region src/hub/HubClientCors.ts
24287
- const MAX_ORIGIN_CHARS = 2048;
24288
- const DESKTOP_RENDERER_ORIGINS$1 = /* @__PURE__ */ new Set(["p4code://app", "p4code-dev://app"]);
24289
- const MAX_PREFLIGHT_HEADER_CHARS = 128;
24290
- const ALLOWED_HEADERS = /* @__PURE__ */ new Set([
24291
- "authorization",
24292
- "content-type",
24293
- "accept"
24294
- ]);
24295
- const CLIENT_ROUTES = {
24296
- "/v1/client-enrollments/exchange": ["POST"],
24297
- "/v1/clients/me": ["GET", "DELETE"],
24298
- "/v1/clients/me/events": ["GET"],
24299
- "/v1/clients/me/socket-ticket": ["POST"],
24300
- "/v1/clients/me/asset-grant": ["POST"],
24301
- "/v1/clients/me/notifications": [
24302
- "GET",
24303
- "PUT",
24304
- "DELETE"
24305
- ],
24306
- "/v1/clients/me/notifications/preferences": ["PUT"],
24307
- "/v1/clients/me/live-activities": ["PUT", "DELETE"]
24308
- };
24309
- const routeMethods = (path) => Object.hasOwn(CLIENT_ROUTES, path) ? CLIENT_ROUTES[path] : void 0;
24310
- const denied$2 = () => HttpServerResponse.jsonUnsafe({ error: "invalid_client_origin" }, {
24311
- status: 403,
24312
- headers: {
24313
- "cache-control": "no-store",
24314
- vary: "Origin"
24315
- }
24316
- });
24317
- /** Arbitrary self-hosted browser clients are allowed; authentication uses explicit credentials, never cookies. */
24318
- function hubClientCorsOrigin(value) {
24319
- if (DESKTOP_RENDERER_ORIGINS$1.has(value)) return value;
24320
- if (value.length > MAX_ORIGIN_CHARS) return void 0;
24321
- try {
24322
- const url = new URL(value);
24323
- if (url.protocol !== "https:" && url.protocol !== "http:" || value !== url.origin || url.username || url.password) return void 0;
24324
- return url.origin;
24325
- } catch {
24326
- return;
24327
- }
24328
- }
24329
- /** Wrap only client-authenticated routes, after their generic failure mapping. */
24330
- const withHubClientCors = (operation) => Effect.gen(function* () {
24331
- const request = yield* HttpServerRequest.HttpServerRequest;
24332
- const path = request.url.split("?")[0];
24333
- if (!routeMethods(path)?.includes(request.method)) return denied$2();
24334
- const raw = request.headers.origin;
24335
- if (raw === void 0) return yield* operation;
24336
- const origin = hubClientCorsOrigin(raw);
24337
- if (!origin) return denied$2();
24338
- const response = yield* operation;
24339
- return response.pipe(HttpServerResponse.setHeaders({
24340
- "access-control-allow-origin": origin,
24341
- "access-control-expose-headers": "retry-after",
24342
- "cache-control": "no-store",
24343
- vary: response.headers.vary ? `${response.headers.vary}, Origin` : "Origin"
24344
- }));
24345
- });
24346
- const hubClientPreflight = Effect.gen(function* () {
24347
- const request = yield* HttpServerRequest.HttpServerRequest;
24348
- const origin = hubClientCorsOrigin(request.headers.origin ?? "");
24349
- const methods = routeMethods(request.url);
24350
- const method = request.headers["access-control-request-method"];
24351
- const raw = request.headers["access-control-request-headers"] ?? "";
24352
- if (raw.length > MAX_PREFLIGHT_HEADER_CHARS) return denied$2();
24353
- const headers = raw === "" ? [] : raw.split(",").map((value) => value.trim().toLowerCase());
24354
- if (!origin || !methods || !method || !methods.includes(method) || raw.length > MAX_PREFLIGHT_HEADER_CHARS || headers.length > ALLOWED_HEADERS.size || new Set(headers).size !== headers.length || headers.some((value) => !ALLOWED_HEADERS.has(value))) return denied$2();
24355
- return HttpServerResponse.empty({
24356
- status: 204,
24357
- headers: {
24358
- "access-control-allow-origin": origin,
24359
- "access-control-allow-methods": methods.join(", "),
24360
- "access-control-allow-headers": "authorization, content-type, accept",
24361
- "cache-control": "no-store",
24362
- vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
24363
- }
24364
- });
24365
- });
24366
- const layer$111 = Layer.mergeAll(Layer.empty, ...Object.keys(CLIENT_ROUTES).map((path) => HttpRouter.add("OPTIONS", path, hubClientPreflight)));
24367
24641
  const HUB_TUNNEL_MAX_FRAME_PAYLOAD_BYTES = 64 * 1024;
24368
24642
  const HUB_TUNNEL_MAX_MESSAGE_BYTES = 128 * 1024 * 1024;
24369
24643
  const HUB_TUNNEL_FRAME_MAGIC = 1345606705;
@@ -24650,7 +24924,7 @@ var HubTunnelConnectorBudget = class {
24650
24924
  const HUB_TUNNEL_RETIRE_TIMEOUT_MS = 1e4;
24651
24925
  var HubTunnelUnavailable = class extends Data.TaggedError("HubTunnelUnavailable") {};
24652
24926
  var HubTunnelRegistry = class extends Context.Service()("@p4code/cli/hub/HubTunnelRegistry") {};
24653
- const layer$110 = Layer.effect(HubTunnelRegistry, Effect.gen(function* () {
24927
+ const layer$111 = Layer.effect(HubTunnelRegistry, Effect.gen(function* () {
24654
24928
  const config = yield* HubConfig;
24655
24929
  const lock = yield* Semaphore.make(1);
24656
24930
  const entries = /* @__PURE__ */ new Map();
@@ -24903,7 +25177,7 @@ const makeHubClientSocketTickets = Effect.gen(function* () {
24903
25177
  };
24904
25178
  });
24905
25179
  var HubClientSocketTickets = class extends Context.Service()("@p4code/cli/hub/HubClientSocketTickets") {};
24906
- const layer$109 = Layer.effect(HubClientSocketTickets, makeHubClientSocketTickets);
25180
+ const layer$110 = Layer.effect(HubClientSocketTickets, makeHubClientSocketTickets);
24907
25181
  //#endregion
24908
25182
  //#region src/hub/HubClientLease.ts
24909
25183
  /** Other Hub processes sharing the database are observed within this interval. */
@@ -24992,7 +25266,7 @@ const readBody$1 = Effect.fn("HubClientRoutes.readBody")(function* (schema, maxi
24992
25266
  }) : request.stream.pipe(Stream.mapError(() => new InvalidClientRequest()))).pipe(Stream.runFoldEffect(() => Buffer.alloc(0), (buffer, chunk) => buffer.byteLength + chunk.byteLength > maximumBytes ? Effect.fail(new InvalidClientRequest()) : Effect.succeed(Buffer.concat([buffer, chunk]))));
24993
25267
  return yield* Schema$1.decodeUnknownEffect(Schema$1.fromJsonString(schema))(body.toString("utf8"), { onExcessProperty: "error" }).pipe(Effect.mapError(() => new InvalidClientRequest()));
24994
25268
  });
24995
- const json$1 = (body) => HttpServerResponse.jsonUnsafe(body, { headers: { "cache-control": "no-store" } });
25269
+ const json$2 = (body) => HttpServerResponse.jsonUnsafe(body, { headers: { "cache-control": "no-store" } });
24996
25270
  const handle$1 = (operation, accountingOwner) => operation.pipe(Effect.timeout(HUB_CLIENT_REQUEST_TIMEOUT_MS), Effect.catch((error) => Effect.gen(function* () {
24997
25271
  if (error instanceof HubThrottledError) return hubThrottledResponse(error.retryAfterSeconds);
24998
25272
  if (error instanceof HubUnauthorizedError) {
@@ -25013,15 +25287,15 @@ const handle$1 = (operation, accountingOwner) => operation.pipe(Effect.timeout(H
25013
25287
  const issue$1 = handle$1(Effect.gen(function* () {
25014
25288
  const principal = yield* authenticateHubServerMembership();
25015
25289
  const input = yield* readBody$1(HubClientEnrollmentInput);
25016
- return json$1(yield* issueHubClientEnrollment(principal, input));
25290
+ return json$2(yield* issueHubClientEnrollment(principal, input));
25017
25291
  }), "server-auth");
25018
25292
  const exchange$1 = handle$1(Effect.gen(function* () {
25019
25293
  const input = yield* readBody$1(HubClientEnrollmentExchange);
25020
- return json$1(yield* consumeHubClientEnrollment(input.secret));
25294
+ return json$2(yield* consumeHubClientEnrollment(input.secret));
25021
25295
  }), "client-route");
25022
25296
  const identity$2 = handle$1(Effect.gen(function* () {
25023
25297
  const request = yield* HttpServerRequest.HttpServerRequest;
25024
- return json$1(yield* authenticateHubClient(readBearerToken(request.headers.authorization)));
25298
+ return json$2(yield* authenticateHubClient(readBearerToken(request.headers.authorization)));
25025
25299
  }), "client-route");
25026
25300
  const events = handle$1(Effect.gen(function* () {
25027
25301
  return yield* hubClientEvents(readBearerToken((yield* HttpServerRequest.HttpServerRequest).headers.authorization));
@@ -25046,7 +25320,7 @@ const socketTicket = handle$1(Effect.gen(function* () {
25046
25320
  yield* authenticateHubClient(credential);
25047
25321
  const input = yield* readBody$1(HubClientSocketTicketInput, MAX_SOCKET_TICKET_REQUEST_BYTES);
25048
25322
  const tickets = yield* HubClientSocketTickets;
25049
- return json$1(yield* tickets.issue(credential, {
25323
+ return json$2(yield* tickets.issue(credential, {
25050
25324
  method: "GET",
25051
25325
  url: `${HUB_TUNNEL_CLIENT_SOCKET_PATH}${input.path}`,
25052
25326
  headers: {
@@ -25059,7 +25333,143 @@ const socketTicket = handle$1(Effect.gen(function* () {
25059
25333
  }
25060
25334
  }));
25061
25335
  }), "client-route");
25062
- const layer$108 = Layer.mergeAll(layer$111, HttpRouter.add("POST", "/v1/clients/me/socket-ticket", withHubClientCors(socketTicket)), HttpRouter.add("POST", "/v1/client-enrollments", issue$1), HttpRouter.add("POST", "/v1/client-enrollments/exchange", withHubClientCors(exchange$1)), HttpRouter.add("GET", "/v1/clients/me", withHubClientCors(identity$2)), HttpRouter.add("GET", "/v1/clients/me/events", withHubClientCors(events)), HttpRouter.add("DELETE", "/v1/clients/me", withHubClientCors(signOut)), HttpRouter.add("POST", "/v1/clients/revoke", revoke));
25336
+ const layer$109 = Layer.mergeAll(layer$113, HttpRouter.add("POST", "/v1/clients/me/socket-ticket", withHubClientCors(socketTicket)), HttpRouter.add("POST", "/v1/client-enrollments", issue$1), HttpRouter.add("POST", "/v1/client-enrollments/exchange", withHubClientCors(exchange$1)), HttpRouter.add("GET", "/v1/clients/me", withHubClientCors(identity$2)), HttpRouter.add("GET", "/v1/clients/me/events", withHubClientCors(events)), HttpRouter.add("DELETE", "/v1/clients/me", withHubClientCors(signOut)), HttpRouter.add("POST", "/v1/clients/revoke", revoke));
25337
+ //#endregion
25338
+ //#region src/hub/HubAnalytics.ts
25339
+ /**
25340
+ * Account-scoped analytics facts: append and read.
25341
+ *
25342
+ * Every row belongs to the authenticated account, and every read is filtered
25343
+ * by it, so one account's facts can never appear in another's page. Appends
25344
+ * are idempotent on the caller's own event id rather than deduplicated by
25345
+ * content: a retry after a dropped response must not double-count, and a fact
25346
+ * is never rewritten once stored.
25347
+ *
25348
+ * @module HubAnalytics
25349
+ */
25350
+ const CURSOR_SEPARATOR = "|";
25351
+ const decodeFact = Schema$1.decodeUnknownEffect(HubAnalyticsFactInput);
25352
+ const decodeQuery = Schema$1.decodeUnknownEffect(HubAnalyticsQuery);
25353
+ const encodePayload$1 = Schema$1.encodeEffect(Schema$1.fromJsonString(Schema$1.Unknown));
25354
+ const decodePayload$1 = Schema$1.decodeUnknownEffect(Schema$1.fromJsonString(Schema$1.Unknown));
25355
+ /** The page position of a row, in the exact order the index reads. */
25356
+ const cursorOf = (row) => `${row.occurred_at}${CURSOR_SEPARATOR}${row.event_id}`;
25357
+ const parseCursor = (cursor) => {
25358
+ const separator = cursor.indexOf(CURSOR_SEPARATOR);
25359
+ if (separator <= 0) return null;
25360
+ const occurredAt = Number.parseInt(cursor.slice(0, separator), 10);
25361
+ const eventId = cursor.slice(separator + 1);
25362
+ return Number.isFinite(occurredAt) && eventId.length > 0 ? {
25363
+ occurredAt,
25364
+ eventId
25365
+ } : null;
25366
+ };
25367
+ /**
25368
+ * Append one fact for the authenticated account.
25369
+ *
25370
+ * The principal is rechecked inside the transaction, the way every other hub
25371
+ * write does it: a credential revoked while the body was being read must not
25372
+ * still land a row.
25373
+ */
25374
+ const appendHubAnalyticsFact = Effect.fn("HubAnalytics.append")(function* (principal, input) {
25375
+ const config = yield* HubConfig;
25376
+ if (!currentPrincipal(config, principal)) return yield* Effect.fail(new HubUnauthorizedError());
25377
+ const fact = yield* decodeFact(input, { onExcessProperty: "error" });
25378
+ const payload = yield* encodePayload$1(fact.payload);
25379
+ const occurredAt = Date.parse(fact.occurredAt);
25380
+ const sql = yield* SqlClient.SqlClient;
25381
+ const now = yield* Clock.currentTimeMillis;
25382
+ return yield* sql.withTransaction(Effect.gen(function* () {
25383
+ if (!currentPrincipal(config, principal)) return yield* Effect.fail(new HubUnauthorizedError());
25384
+ if ((yield* sql`SELECT event_id FROM hub_analytics_facts
25385
+ WHERE account_id = ${principal.accountId} AND event_id = ${fact.eventId}`).length > 0) return { accepted: false };
25386
+ yield* sql`INSERT INTO hub_analytics_facts
25387
+ (account_id, environment_id, event_id, kind, version, occurred_at, payload, created_at)
25388
+ VALUES (${principal.accountId}, ${principal.environmentId}, ${fact.eventId}, ${fact.kind},
25389
+ ${fact.version}, ${occurredAt}, ${payload}, ${now})
25390
+ ON CONFLICT(account_id, event_id) DO NOTHING`;
25391
+ return { accepted: true };
25392
+ }));
25393
+ });
25394
+ /**
25395
+ * Read this account's facts, newest first.
25396
+ *
25397
+ * Paged by keyset rather than offset, so a fact appended between two pages
25398
+ * cannot shift a row into being read twice or skipped. The limit is bounded by
25399
+ * the contract, and one extra row is read to decide whether a next page
25400
+ * exists without a second count query.
25401
+ */
25402
+ const queryHubAnalyticsFacts = Effect.fn("HubAnalytics.query")(function* (principal, input) {
25403
+ if (!currentPrincipal(yield* HubConfig, principal)) return yield* Effect.fail(new HubUnauthorizedError());
25404
+ const query = yield* decodeQuery(input, { onExcessProperty: "error" });
25405
+ const limit = query.limit ?? HUB_ANALYTICS_QUERY_LIMITS.defaultLimit;
25406
+ const kinds = query.kinds === void 0 ? null : `,${query.kinds.join(",")},`;
25407
+ const since = query.since === void 0 ? null : Date.parse(query.since);
25408
+ const until = query.until === void 0 ? null : Date.parse(query.until);
25409
+ const cursor = query.cursor === void 0 ? null : parseCursor(query.cursor);
25410
+ const cursorOccurredAt = cursor === null ? null : cursor.occurredAt;
25411
+ const cursorEventId = cursor === null ? null : cursor.eventId;
25412
+ const rows = yield* (yield* SqlClient.SqlClient)`
25413
+ SELECT environment_id, event_id, kind, version, occurred_at, payload
25414
+ FROM hub_analytics_facts
25415
+ WHERE account_id = ${principal.accountId}
25416
+ AND (${kinds} IS NULL OR instr(${kinds}, ',' || kind || ',') > 0)
25417
+ AND (${since} IS NULL OR occurred_at >= ${since})
25418
+ AND (${until} IS NULL OR occurred_at <= ${until})
25419
+ AND (
25420
+ ${cursorOccurredAt} IS NULL
25421
+ OR occurred_at < ${cursorOccurredAt}
25422
+ OR (occurred_at = ${cursorOccurredAt} AND event_id < ${cursorEventId})
25423
+ )
25424
+ ORDER BY occurred_at DESC, event_id DESC
25425
+ LIMIT ${limit + 1}`;
25426
+ const page = rows.slice(0, limit);
25427
+ const facts = [];
25428
+ for (const row of page) facts.push({
25429
+ eventId: row.event_id,
25430
+ environmentId: row.environment_id,
25431
+ kind: row.kind,
25432
+ version: row.version,
25433
+ occurredAt: new Date(row.occurred_at).toISOString(),
25434
+ payload: yield* decodePayload$1(row.payload)
25435
+ });
25436
+ const last = page[page.length - 1];
25437
+ return {
25438
+ facts,
25439
+ nextCursor: rows.length > limit && last !== void 0 ? cursorOf(last) : null
25440
+ };
25441
+ });
25442
+ //#endregion
25443
+ //#region src/hub/HubAnalyticsRoutes.ts
25444
+ /**
25445
+ * The account's own analytics endpoints.
25446
+ *
25447
+ * Both routes require the same server membership every other environment-owned
25448
+ * hub write uses, and neither forwards anything anywhere: the hub is the only
25449
+ * sink for these facts.
25450
+ *
25451
+ * @module HubAnalyticsRoutes
25452
+ */
25453
+ const MAX_ANALYTICS_BODY_BYTES = 16 * 1024;
25454
+ const json$1 = (value) => HttpServerResponse.jsonUnsafe(value, { headers: { "cache-control": "no-store" } });
25455
+ /** Only the filters the query contract knows; anything else is a decode failure. */
25456
+ const queryFromUrl = (url) => Object.fromEntries([
25457
+ ["kinds", url.searchParams.getAll("kind")],
25458
+ ["since", url.searchParams.get("since")],
25459
+ ["until", url.searchParams.get("until")],
25460
+ ["limit", url.searchParams.get("limit")],
25461
+ ["cursor", url.searchParams.get("cursor")]
25462
+ ].filter(([, value]) => Array.isArray(value) ? value.length > 0 : value !== null).map(([key, value]) => key === "limit" && typeof value === "string" ? [key, Number.parseInt(value, 10)] : [key, value]));
25463
+ const layer$108 = Layer.mergeAll(HttpRouter.add("POST", "/v1/analytics/facts", handle$1(Effect.gen(function* () {
25464
+ const principal = yield* authenticateHubServerMembership();
25465
+ const input = yield* readBody$1(HubAnalyticsFactInput, MAX_ANALYTICS_BODY_BYTES);
25466
+ return json$1(yield* appendHubAnalyticsFact(principal, input));
25467
+ }), "server-auth")), HttpRouter.add("GET", "/v1/analytics/facts", handle$1(Effect.gen(function* () {
25468
+ const principal = yield* authenticateHubServerMembership();
25469
+ const request = yield* HttpServerRequest.HttpServerRequest;
25470
+ const url = new URL(request.url, "http://hub.invalid");
25471
+ return json$1(yield* queryHubAnalyticsFacts(principal, queryFromUrl(url)));
25472
+ }), "server-auth")));
25063
25473
  //#endregion
25064
25474
  //#region src/hub/HubPushTokenProtection.ts
25065
25475
  const KEY_NAME = "hub-push-token-encryption-v1";
@@ -26812,7 +27222,7 @@ const layer$100 = Layer.effect(HubTunnelPublished, Effect.gen(function* () {
26812
27222
  }));
26813
27223
  //#endregion
26814
27224
  //#region src/hub/HubTunnelCors.ts
26815
- const MAX_HEADER_CHARS = 256;
27225
+ const MAX_HEADER_CHARS$1 = 256;
26816
27226
  const METHODS = [
26817
27227
  "GET",
26818
27228
  "HEAD",
@@ -26842,7 +27252,7 @@ function hubTunnelCors(method, headers) {
26842
27252
  };
26843
27253
  if (method === "OPTIONS" && headers["access-control-request-method"] !== void 0) {
26844
27254
  const raw = headers["access-control-request-headers"] ?? "";
26845
- if (raw.length > MAX_HEADER_CHARS) return null;
27255
+ if (raw.length > MAX_HEADER_CHARS$1) return null;
26846
27256
  const requested = raw ? raw.split(",").map((name) => name.trim().toLowerCase()) : [];
26847
27257
  if (!METHODS.includes(headers["access-control-request-method"]) || requested.length > HEADERS.length || new Set(requested).size !== requested.length || requested.some((name) => !HEADERS.includes(name))) return null;
26848
27258
  result["access-control-allow-methods"] = METHODS.join(", ");
@@ -28597,8 +29007,8 @@ const releaseFeedLeaseRoute = HttpRouter.add("DELETE", "/feed/lease", respondToH
28597
29007
  return HttpServerResponse.empty({ status: 204 });
28598
29008
  })));
28599
29009
  /** Provide around HttpRouter.serve so request fibers share these process-owned services. */
28600
- const routingServices = Layer.mergeAll(layer$112, layer$109, layer$101, layer$100).pipe(Layer.provideMerge(layer$110));
28601
- const layer$94 = Layer.mergeAll(layer$99, layer$98, layer$102, layer$96, layer$95, layer$108, layer$97, healthRoute, identityRoute, listTasksRoute, getTaskRoute, createTaskRoute, putTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute, listFeedSourcesRoute, upsertFeedSourceRoute, deleteFeedSourceRoute, listFeedRoute, putFeedArticleRoute, putFeedEngagementRoute, markFeedReadRoute, cleanupFeedRoute, setFeedRefreshedAtRoute, acquireFeedLeaseRoute, renewFeedLeaseRoute, releaseFeedLeaseRoute).pipe(Layer.provideMerge(layer$112), Layer.provideMerge(layer$109), Layer.provideMerge(layer$101));
29010
+ const routingServices = Layer.mergeAll(layer$112, layer$110, layer$101, layer$100).pipe(Layer.provideMerge(layer$111));
29011
+ const layer$94 = Layer.mergeAll(layer$99, layer$98, layer$102, layer$96, layer$95, layer$109, layer$97, healthRoute, identityRoute, listTasksRoute, getTaskRoute, createTaskRoute, putTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute, listFeedSourcesRoute, upsertFeedSourceRoute, deleteFeedSourceRoute, listFeedRoute, putFeedArticleRoute, putFeedEngagementRoute, markFeedReadRoute, cleanupFeedRoute, setFeedRefreshedAtRoute, acquireFeedLeaseRoute, renewFeedLeaseRoute, releaseFeedLeaseRoute).pipe(Layer.provideMerge(layer$112), Layer.provideMerge(layer$110), Layer.provideMerge(layer$101));
28602
29012
  //#endregion
28603
29013
  //#region src/hub/Migrations/001_Tasks.ts
28604
29014
  /**
@@ -29080,6 +29490,21 @@ var _021_NotificationDeliveryProgress_default = Effect.gen(function* () {
29080
29490
  yield* (yield* SqlClient.SqlClient)`ALTER TABLE hub_notification_deliveries ADD COLUMN app_delivered INTEGER NOT NULL DEFAULT 0`;
29081
29491
  });
29082
29492
  //#endregion
29493
+ //#region src/hub/Migrations/022_AnalyticsFacts.ts
29494
+ var _022_AnalyticsFacts_default = Effect.gen(function* () {
29495
+ const sql = yield* SqlClient.SqlClient;
29496
+ yield* sql`CREATE TABLE hub_analytics_facts (
29497
+ account_id TEXT NOT NULL, environment_id TEXT NOT NULL, event_id TEXT NOT NULL,
29498
+ kind TEXT NOT NULL, version INTEGER NOT NULL, occurred_at INTEGER NOT NULL,
29499
+ payload TEXT NOT NULL, created_at INTEGER NOT NULL,
29500
+ PRIMARY KEY(account_id, event_id)
29501
+ )`;
29502
+ yield* sql`CREATE INDEX hub_analytics_facts_recent
29503
+ ON hub_analytics_facts(account_id, occurred_at DESC, event_id DESC)`;
29504
+ yield* sql`CREATE INDEX hub_analytics_facts_kind
29505
+ ON hub_analytics_facts(account_id, kind, occurred_at DESC, event_id DESC)`;
29506
+ });
29507
+ //#endregion
29083
29508
  //#region src/hub/Migrations.ts
29084
29509
  /**
29085
29510
  * Hub migrations.
@@ -29195,6 +29620,11 @@ const hubMigrationEntries = [
29195
29620
  21,
29196
29621
  "NotificationDeliveryProgress",
29197
29622
  _021_NotificationDeliveryProgress_default
29623
+ ],
29624
+ [
29625
+ 22,
29626
+ "AnalyticsFacts",
29627
+ _022_AnalyticsFacts_default
29198
29628
  ]
29199
29629
  ];
29200
29630
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -29286,7 +29716,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
29286
29716
  });
29287
29717
  if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is reachable beyond loopback. The bearer token is the only control — repeated auth failures are not rate limited.", { host: config.host });
29288
29718
  }));
29289
- const makeHubLayer = HttpRouter.serve(Layer.mergeAll(layer$94, layer$106)).pipe(Layer.provideMerge(routingServices), Layer.provideMerge(layer$104), Layer.provideMerge(layer$105), Layer.provideMerge(layer$103), Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(layer$113), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
29719
+ const makeHubLayer = HttpRouter.serve(Layer.mergeAll(layer$94, layer$106, layer$108)).pipe(Layer.provideMerge(routingServices), Layer.provideMerge(layer$104), Layer.provideMerge(layer$105), Layer.provideMerge(layer$103), Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(layer$114), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
29290
29720
  const runHub = Layer.launch(makeHubLayer);
29291
29721
  //#endregion
29292
29722
  //#region src/atomicWrite.ts
@@ -30502,6 +30932,34 @@ Layer.succeed(ClaudeMcpFiles, {
30502
30932
  const PROBE_CACHE_TTL_MS = 300 * 1e3;
30503
30933
  /** An authorize URL nobody returned from within this window is abandoned. */
30504
30934
  const PENDING_FLOW_TTL_MS = 600 * 1e3;
30935
+ /**
30936
+ * The two answers that mean the credential, not the request, was refused.
30937
+ *
30938
+ * The same pair `McpToolClient` reports as "the server rejected p4code's
30939
+ * credentials": 401 for a token the server will not accept, 403 for one it
30940
+ * accepts and will not act on. Both leave the session with a server it cannot
30941
+ * use, so both take the one refresh and, failing that, the sign-in.
30942
+ */
30943
+ const credentialsRefused = (status) => status === 401 || status === 403;
30944
+ /**
30945
+ * Whether a token endpoint's answer means the grant itself is gone.
30946
+ *
30947
+ * RFC 6749 gives a retired or rejected refresh token one spelling: 400 with
30948
+ * `error: "invalid_grant"`. The other 400s - `invalid_request`,
30949
+ * `unsupported_grant_type`, `invalid_scope` - are complaints about the request
30950
+ * this code sent, and erasing the user's credential over a malformed request
30951
+ * of ours would charge them a sign-in for a bug. 401 and 403 stay terminal:
30952
+ * the client credential itself was refused, and no retry of this shape fixes
30953
+ * that. Everything else - 429, a 5xx, whatever a proxy invents - is the server
30954
+ * having a bad minute.
30955
+ */
30956
+ const terminalRefreshAnswer = (status, json) => {
30957
+ if (status === 401 || status === 403) return true;
30958
+ if (status !== 400) return false;
30959
+ return (typeof json === "object" && json !== null ? json["error"] : void 0) === "invalid_grant";
30960
+ };
30961
+ /** Why one refresh attempt failed, and whether the stored grant survives it. */
30962
+ var RefreshFailure = class extends Data.TaggedError("RefreshFailure") {};
30505
30963
  /** A token this close to expiry is refreshed rather than handed to a session. */
30506
30964
  const TOKEN_EXPIRY_MARGIN_MS = 60 * 1e3;
30507
30965
  /** Outbound discovery, registration and token requests all use this cap. */
@@ -30613,6 +31071,7 @@ const make$109 = Effect.gen(function* () {
30613
31071
  const crypto = yield* Crypto.Crypto;
30614
31072
  const pendingFlows = yield* Ref.make(/* @__PURE__ */ new Map());
30615
31073
  const probeCache = yield* Ref.make(/* @__PURE__ */ new Map());
31074
+ const tokenVerdicts = yield* Ref.make(/* @__PURE__ */ new Map());
30616
31075
  const redirectUri = `http://127.0.0.1:${config.port}${OAUTH_CALLBACK_PATH}`;
30617
31076
  const nowMillis = Effect.map(DateTime.now, DateTime.toEpochMillis);
30618
31077
  const readRecord = Effect.fn("McpOAuth.readRecord")(function* (server) {
@@ -30644,26 +31103,28 @@ const make$109 = Effect.gen(function* () {
30644
31103
  * does — while answering the initialize POST with the 401 challenge this
30645
31104
  * probe exists to see.
30646
31105
  */
31106
+ const initializeRequest = (registration, accessToken) => HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.setHeaders(HttpClientRequest.post(registration.url), {
31107
+ ...registration.headers,
31108
+ ...accessToken === void 0 ? {} : { authorization: `Bearer ${accessToken}` },
31109
+ accept: "application/json, text/event-stream"
31110
+ }), {
31111
+ jsonrpc: "2.0",
31112
+ id: 0,
31113
+ method: "initialize",
31114
+ params: {
31115
+ protocolVersion: MCP_PROBE_PROTOCOL_VERSION,
31116
+ capabilities: {},
31117
+ clientInfo: {
31118
+ name: "p4code",
31119
+ version: "0.0.0"
31120
+ }
31121
+ }
31122
+ });
30647
31123
  const probe = Effect.fn("McpOAuth.probe")(function* (registration) {
30648
31124
  const now = yield* nowMillis;
30649
31125
  const cached = (yield* Ref.get(probeCache)).get(registration.url);
30650
31126
  if (cached !== void 0 && now - cached.at < PROBE_CACHE_TTL_MS) return cached;
30651
- const verdict = yield* http.execute(HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.setHeaders(HttpClientRequest.post(registration.url), {
30652
- ...registration.headers,
30653
- accept: "application/json, text/event-stream"
30654
- }), {
30655
- jsonrpc: "2.0",
30656
- id: 0,
30657
- method: "initialize",
30658
- params: {
30659
- protocolVersion: MCP_PROBE_PROTOCOL_VERSION,
30660
- capabilities: {},
30661
- clientInfo: {
30662
- name: "p4code",
30663
- version: "0.0.0"
30664
- }
30665
- }
30666
- })).pipe(Effect.timeout(OAUTH_REQUEST_TIMEOUT_MS), Effect.map((response) => {
31127
+ const verdict = yield* http.execute(initializeRequest(registration)).pipe(Effect.timeout(OAUTH_REQUEST_TIMEOUT_MS), Effect.map((response) => {
30667
31128
  const challengeHeader = response.headers["www-authenticate"];
30668
31129
  const challenge = response.status === 401 && challengeHeader !== void 0 ? parseWwwAuthenticate(challengeHeader) : void 0;
30669
31130
  return {
@@ -30690,12 +31151,53 @@ const make$109 = Effect.gen(function* () {
30690
31151
  next.delete(url);
30691
31152
  return next;
30692
31153
  });
31154
+ /**
31155
+ * Ask the server whether the token this machine holds still works.
31156
+ *
31157
+ * `probe` cannot answer this. It asks without credentials, so its 401 is the
31158
+ * one every protected server sends and says nothing about the token sitting
31159
+ * in the record. A token revoked at the other end, or issued for a workspace
31160
+ * the user has since left, stays well inside its stored expiry and is
31161
+ * refused on every call - the state this catches.
31162
+ *
31163
+ * Unreachable counts as usable, exactly as `probe` treats it: a network
31164
+ * fault is not a revocation, and signing someone out of a server that is
31165
+ * merely down helps nobody. Only a real answer is remembered.
31166
+ */
31167
+ const validateToken = Effect.fn("McpOAuth.validateToken")(function* (registration, accessToken) {
31168
+ const now = yield* nowMillis;
31169
+ const cached = (yield* Ref.get(tokenVerdicts)).get(registration.name);
31170
+ if (cached !== void 0 && cached.accessToken === accessToken) {
31171
+ if (!cached.accepted) return false;
31172
+ if (now - cached.at < PROBE_CACHE_TTL_MS) return true;
31173
+ }
31174
+ const answered = yield* http.execute(initializeRequest(registration, accessToken)).pipe(Effect.timeout(OAUTH_REQUEST_TIMEOUT_MS), Effect.map((response) => Option.some(!credentialsRefused(response.status))), Effect.orElseSucceed(() => Option.none()));
31175
+ if (Option.isNone(answered)) return true;
31176
+ yield* Ref.update(tokenVerdicts, (cache) => {
31177
+ const next = new Map(cache);
31178
+ next.set(registration.name, {
31179
+ accessToken,
31180
+ at: now,
31181
+ accepted: answered.value
31182
+ });
31183
+ return next;
31184
+ });
31185
+ return answered.value;
31186
+ });
31187
+ /** Whether the server has refused the exact token the record still holds. */
31188
+ const rejectedAlready = (server, accessToken) => Effect.map(Ref.get(tokenVerdicts), (cache) => {
31189
+ const verdict = cache.get(server);
31190
+ return verdict !== void 0 && verdict.accessToken === accessToken && !verdict.accepted;
31191
+ });
30693
31192
  const statusFor = (registration) => Effect.gen(function* () {
30694
31193
  const record = yield* readRecord(registration.name);
30695
- if (Option.isSome(record) && record.value.tokens !== void 0) return Option.some({
30696
- required: true,
30697
- connected: true
30698
- });
31194
+ if (Option.isSome(record) && record.value.tokens !== void 0) {
31195
+ const rejected = yield* rejectedAlready(registration.name, record.value.tokens.accessToken);
31196
+ return Option.some({
31197
+ required: true,
31198
+ connected: !rejected
31199
+ });
31200
+ }
30699
31201
  if ((yield* probe(registration)).requiresAuth || Option.isSome(record)) return Option.some({
30700
31202
  required: true,
30701
31203
  connected: false
@@ -30870,6 +31372,24 @@ const make$109 = Effect.gen(function* () {
30870
31372
  ok: false,
30871
31373
  detail: "The sign-in failed. See the server log."
30872
31374
  }))));
31375
+ /**
31376
+ * Drop the tokens the server will not accept, keeping the client
31377
+ * registration so the sign-in that follows skips re-registration.
31378
+ *
31379
+ * Compare-and-delete rather than a blind write, because two sessions can
31380
+ * resolve the same server at once. The loser of that race spends a refresh
31381
+ * token the winner has already rotated, is told `invalid_grant`, and would
31382
+ * otherwise erase the credential the winner just stored - turning a refresh
31383
+ * that worked into a sign-in prompt. A record whose access token has moved on
31384
+ * belongs to somebody else's successful refresh and is left alone.
31385
+ */
31386
+ const forgetTokens = Effect.fn("McpOAuth.forgetTokens")(function* (server, refusedAccessToken) {
31387
+ const current = yield* readRecord(server);
31388
+ if (Option.isNone(current)) return;
31389
+ if (current.value.tokens?.accessToken !== refusedAccessToken) return;
31390
+ const { tokens: _refused, ...rest } = current.value;
31391
+ yield* writeRecord(server, rest);
31392
+ });
30873
31393
  const disconnect = (name) => Effect.gen(function* () {
30874
31394
  const record = yield* readRecord(name);
30875
31395
  if (Option.isNone(record)) return;
@@ -30887,9 +31407,15 @@ const make$109 = Effect.gen(function* () {
30887
31407
  ...record.clientSecret === void 0 ? {} : { client_secret: record.clientSecret },
30888
31408
  ...record.resource === void 0 ? {} : { resource: record.resource }
30889
31409
  }));
30890
- if (status !== 200) return yield* Effect.fail(`status ${status}`);
31410
+ if (status !== 200) return yield* Effect.fail(new RefreshFailure({
31411
+ terminal: terminalRefreshAnswer(status, json),
31412
+ detail: `status ${status}`
31413
+ }));
30891
31414
  const decoded = decodeTokenResponse(json);
30892
- if (decoded._tag !== "Success") return yield* Effect.fail("unexpected shape");
31415
+ if (decoded._tag !== "Success") return yield* Effect.fail(new RefreshFailure({
31416
+ terminal: false,
31417
+ detail: "unexpected shape"
31418
+ }));
30893
31419
  const response = decoded.value;
30894
31420
  const now = yield* nowMillis;
30895
31421
  const tokens = {
@@ -30908,9 +31434,16 @@ const make$109 = Effect.gen(function* () {
30908
31434
  if (Option.isNone(record) || record.value.tokens === void 0) return Option.none();
30909
31435
  const tokens = record.value.tokens;
30910
31436
  const now = yield* nowMillis;
30911
- if (!(tokens.expiresAt !== void 0 && now >= tokens.expiresAt - TOKEN_EXPIRY_MARGIN_MS)) return Option.some(tokens.accessToken);
30912
- if (tokens.refreshToken === void 0) return Option.none();
30913
- return yield* refreshTokens(registration.name, record.value, tokens.refreshToken).pipe(Effect.map(Option.some), Effect.catchCause((cause) => Effect.logWarning("mcp oauth refresh failed", {
31437
+ if (!(tokens.expiresAt !== void 0 && now >= tokens.expiresAt - TOKEN_EXPIRY_MARGIN_MS) && (yield* validateToken(registration, tokens.accessToken))) return Option.some(tokens.accessToken);
31438
+ if (tokens.refreshToken === void 0) {
31439
+ yield* forgetTokens(registration.name, tokens.accessToken);
31440
+ return Option.none();
31441
+ }
31442
+ return yield* refreshTokens(registration.name, record.value, tokens.refreshToken).pipe(Effect.map(Option.some), Effect.catchTag("RefreshFailure", (failure) => Effect.logWarning("mcp oauth refresh failed", {
31443
+ server: registration.name,
31444
+ detail: failure.detail,
31445
+ terminal: failure.terminal
31446
+ }).pipe(Effect.andThen(failure.terminal ? forgetTokens(registration.name, tokens.accessToken) : Effect.void), Effect.as(Option.none()))), Effect.catchCause((cause) => Effect.logWarning("mcp oauth refresh could not be attempted", {
30914
31447
  cause,
30915
31448
  server: registration.name
30916
31449
  }).pipe(Effect.as(Option.none()))));
@@ -30944,6 +31477,16 @@ const decodeRegistration$1 = Schema$1.decodeUnknownExit(RegistrationFromJson$1);
30944
31477
  const encodeRegistration = Schema$1.encodeSync(RegistrationFromJson$1);
30945
31478
  var McpRegistry = class extends Context.Service()("@p4code/cli/mcp/McpRegistry") {};
30946
31479
  const slotsOf = (registration) => registration.secrets ?? [];
31480
+ /**
31481
+ * What a session is told when a server is left out for want of a credential.
31482
+ *
31483
+ * The name and where the row came from, and nothing else: a token, a url query
31484
+ * or a header would end up in a log file that outlives the session. The action
31485
+ * names the one place a person can fix it, because "absent" on its own reads as
31486
+ * a bug rather than as a sign-in that has not happened.
31487
+ */
31488
+ const OMITTED_FOR_SIGN_IN = "mcp server omitted: sign-in required";
31489
+ const SIGN_IN_ACTION = "Connect it in Settings > MCP servers.";
30947
31490
  const make$108 = Effect.gen(function* () {
30948
31491
  const config = yield* ServerConfig$1;
30949
31492
  const secrets = yield* ServerSecretStore;
@@ -31060,6 +31603,14 @@ const make$108 = Effect.gen(function* () {
31060
31603
  if (registration.transport !== "stdio" && !hasExplicitAuthorization) {
31061
31604
  const token = yield* oauth.accessTokenFor(registration);
31062
31605
  if (Option.isSome(token)) headers["Authorization"] = `Bearer ${token.value}`;
31606
+ else if (Option.isSome(yield* oauth.statusFor(registration))) {
31607
+ yield* Effect.logWarning(OMITTED_FOR_SIGN_IN, {
31608
+ server: registration.name,
31609
+ source: "registration",
31610
+ action: SIGN_IN_ACTION
31611
+ });
31612
+ continue;
31613
+ }
31063
31614
  }
31064
31615
  resolved[registration.name] = registration.transport === "stdio" ? {
31065
31616
  type: "stdio",
@@ -31095,7 +31646,15 @@ const make$108 = Effect.gen(function* () {
31095
31646
  if (!Object.keys(headers).some((key) => key.toLowerCase() === "authorization")) {
31096
31647
  const token = yield* oauth.accessTokenFor(userScope);
31097
31648
  if (Option.isSome(token)) headers["Authorization"] = `Bearer ${token.value}`;
31098
- else if (Option.isSome(yield* oauth.statusFor(userScope))) continue;
31649
+ else if (Option.isSome(yield* oauth.statusFor(userScope))) {
31650
+ yield* Effect.logWarning(OMITTED_FOR_SIGN_IN, {
31651
+ server: userScope.name,
31652
+ source: "user-scope",
31653
+ sessionName,
31654
+ action: SIGN_IN_ACTION
31655
+ });
31656
+ continue;
31657
+ }
31099
31658
  }
31100
31659
  resolved[sessionName] = {
31101
31660
  type: userScope.transport,
@@ -33858,7 +34417,7 @@ const findAgentPreset = (requested) => {
33858
34417
  };
33859
34418
  //#endregion
33860
34419
  //#region src/cli/assets.ts
33861
- const SyncServicesLive = layer$87.pipe(Layer.provideMerge(layer$88), Layer.provideMerge(layer$89), Layer.provideMerge(layer$93.pipe(Layer.provide(layer$118))), Layer.provideMerge(layer$118), Layer.provideMerge(FetchHttpClient.layer));
34420
+ const SyncServicesLive = layer$87.pipe(Layer.provideMerge(layer$88), Layer.provideMerge(layer$89), Layer.provideMerge(layer$93.pipe(Layer.provide(layer$119))), Layer.provideMerge(layer$119), Layer.provideMerge(FetchHttpClient.layer));
33862
34421
  const runWithSync = (flags, run) => Effect.gen(function* () {
33863
34422
  const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
33864
34423
  return yield* Effect.gen(function* () {
@@ -33866,7 +34425,7 @@ const runWithSync = (flags, run) => Effect.gen(function* () {
33866
34425
  sync: yield* AssetSync,
33867
34426
  link: yield* HubLink
33868
34427
  });
33869
- }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$121(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
34428
+ }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$122(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
33870
34429
  });
33871
34430
  const STATE_LABEL = {
33872
34431
  synced: "synced",
@@ -36240,7 +36799,11 @@ function projectEvent(model, event) {
36240
36799
  case "thread.activity-appended": return decodeForEvent(ThreadActivityAppendedPayload, event.payload, event.type, "payload").pipe(Effect.map((payload) => {
36241
36800
  const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
36242
36801
  if (!thread) return nextBase;
36243
- const activities = [...thread.activities.filter((entry) => entry.id !== payload.activity.id), payload.activity].toSorted(compareThreadActivities).slice(-500);
36802
+ const appended = payload.activity.sequence === void 0 ? {
36803
+ ...payload.activity,
36804
+ sequence: event.sequence
36805
+ } : payload.activity;
36806
+ const activities = [...thread.activities.filter((entry) => entry.id !== appended.id), appended].toSorted(compareThreadActivities).slice(-500);
36244
36807
  return {
36245
36808
  ...nextBase,
36246
36809
  threads: updateThread(nextBase.threads, payload.threadId, {
@@ -40666,7 +41229,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
40666
41229
  kind: activity.kind,
40667
41230
  summary: activity.summary,
40668
41231
  payload: activity.payload,
40669
- ...activity.sequence !== void 0 ? { sequence: activity.sequence } : {},
41232
+ sequence: activity.sequence ?? event.sequence,
40670
41233
  createdAt: activity.createdAt
40671
41234
  });
40672
41235
  return;
@@ -47399,7 +47962,7 @@ const startDeviceHub = Effect.fn("DeviceHubProcess.start")(function* (input) {
47399
47962
  });
47400
47963
  //#endregion
47401
47964
  //#region src/device/AgentDeviceProcessLive.ts
47402
- const decodeJson$2 = Schema$1.decodeUnknownSync(Schema$1.UnknownFromJsonString);
47965
+ const decodeJson$3 = Schema$1.decodeUnknownSync(Schema$1.UnknownFromJsonString);
47403
47966
  var AgentDeviceProcessLiveFailure = class extends Data.TaggedError("AgentDeviceProcessLiveFailure") {};
47404
47967
  const READY_TIMEOUT_MS = 3e4;
47405
47968
  const MAX_READY_LINE = 256;
@@ -47511,7 +48074,7 @@ const make$93 = Effect.fn("AgentDeviceProcess.make")(function* () {
47511
48074
  maxBytes: MAX_RESPONSE_BYTES$7
47512
48075
  });
47513
48076
  if (response.status !== 200 || body.truncated || body.invalidUtf8) return yield* new AgentDeviceProcessLiveFailure({ message: "Agent authorization failed" });
47514
- const result = yield* Effect.try(() => decodeJson$2(body.text));
48077
+ const result = yield* Effect.try(() => decodeJson$3(body.text));
47515
48078
  if (!result || typeof result !== "object" || !("id" in result) || result.id !== "p4-readiness" || !("result" in result) || !result.result || typeof result.result !== "object" || !("ok" in result.result) || result.result.ok !== true) return yield* new AgentDeviceProcessLiveFailure({ message: "Agent readiness rejected" });
47516
48079
  endpoint = {
47517
48080
  baseUrl,
@@ -49511,7 +50074,7 @@ main().catch(() => { if (!stopping) emit({ phase: 'failed', message: 'Remote dev
49511
50074
  }
49512
50075
  //#endregion
49513
50076
  //#region src/device/SshDeviceConnection.ts
49514
- const decodeJson$1 = Schema$1.decodeUnknownSync(Schema$1.UnknownFromJsonString);
50077
+ const decodeJson$2 = Schema$1.decodeUnknownSync(Schema$1.UnknownFromJsonString);
49515
50078
  const START_TIMEOUT_MS = 66e4;
49516
50079
  const FORWARD_TIMEOUT_MS = 15e3;
49517
50080
  const VERIFY_TIMEOUT_MS = 3e4;
@@ -49666,7 +50229,7 @@ const connectSshDevice = Effect.fn("SshDeviceConnection.connect")(function* (inp
49666
50229
  maxBytes: MAX_RESPONSE_BYTES$4
49667
50230
  });
49668
50231
  if (response.status !== 200 || body.truncated || body.invalidUtf8) return yield* fail("verifying forwarded agent")(void 0);
49669
- const result = yield* Effect.try(() => decodeJson$1(body.text));
50232
+ const result = yield* Effect.try(() => decodeJson$2(body.text));
49670
50233
  if (!result || typeof result !== "object" || !("id" in result) || result.id !== identity.nonce || !("result" in result) || !result.result || typeof result.result !== "object" || !("ok" in result.result) || result.result.ok !== true) return yield* fail("authenticating forwarded agent")(void 0);
49671
50234
  }).pipe(Effect.scoped, Effect.timeout(Duration.millis(VERIFY_TIMEOUT_MS)), Effect.mapError(fail("verifying forwarded agent")));
49672
50235
  return {
@@ -51095,7 +51658,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
51095
51658
  });
51096
51659
  yield* Console.log(output);
51097
51660
  }));
51098
- const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$121(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
51661
+ const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$122(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
51099
51662
  return yield* Effect.gen(function* () {
51100
51663
  const snapshot = yield* getOfflineSnapshot();
51101
51664
  const orchestrationEngine = yield* OrchestrationEngineService;
@@ -51106,7 +51669,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
51106
51669
  });
51107
51670
  yield* Console.log(output);
51108
51671
  }).pipe(Effect.provide(offlineRuntimeLayer));
51109
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$70).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$121(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
51672
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$70).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$122(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
51110
51673
  });
51111
51674
  const projectAddCommand = Command.make("add", {
51112
51675
  ...projectLocationFlags,
@@ -59682,7 +60245,7 @@ const TYPESAFE_REQUEST_BUDGET_TOKENS = 32e3;
59682
60245
  const TYPESAFE_BUDGET_RESERVE_TOKENS = 2e3;
59683
60246
  var TypeSafeRequestError = class extends Data.TaggedError("TypeSafeRequestError") {};
59684
60247
  var TypeSafeClient = class extends Context.Service()("@p4code/cli/orchestration/triage/TypeSafeClient") {};
59685
- const asRecord$8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
60248
+ const asRecord$10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
59686
60249
  /**
59687
60250
  * The finite numbers out of a probability map, or nothing.
59688
60251
  *
@@ -59691,7 +60254,7 @@ const asRecord$8 = (value) => typeof value === "object" && value !== null && !Ar
59691
60254
  * invented number is worse than one built on fewer.
59692
60255
  */
59693
60256
  function readProbabilities(value) {
59694
- const record = asRecord$8(value);
60257
+ const record = asRecord$10(value);
59695
60258
  if (record === void 0) return void 0;
59696
60259
  const probabilities = {};
59697
60260
  for (const [key, entry] of Object.entries(record)) if (typeof entry === "number" && Number.isFinite(entry)) probabilities[key] = entry;
@@ -59705,7 +60268,7 @@ function readProbabilities(value) {
59705
60268
  * can never reach the caller wearing a confidence it did not have.
59706
60269
  */
59707
60270
  function parseTypeSafeAnswer(value) {
59708
- const record = asRecord$8(value);
60271
+ const record = asRecord$10(value);
59709
60272
  if (record === void 0) return void 0;
59710
60273
  const confidence = record["confidence"];
59711
60274
  switch (record["type"]) {
@@ -59746,7 +60309,7 @@ function parseTypeSafeAnswer(value) {
59746
60309
  }
59747
60310
  /** The one answer for the question that was asked, out of the answers map. */
59748
60311
  function readAnswerFor(body, questionId) {
59749
- const answers = asRecord$8(asRecord$8(body)?.["answers"]);
60312
+ const answers = asRecord$10(asRecord$10(body)?.["answers"]);
59750
60313
  return answers === void 0 ? void 0 : parseTypeSafeAnswer(answers[questionId]);
59751
60314
  }
59752
60315
  const makeTypeSafeClient = Effect.gen(function* () {
@@ -59850,19 +60413,19 @@ const estimateTokens$1 = (text) => Math.ceil(text.length / 4);
59850
60413
  /** One line per event is plenty to answer "did anything reviewable happen". */
59851
60414
  const MAX_LINE_CHARS = 400;
59852
60415
  const oneLine = (text) => text.replace(/\s+/gu, " ").trim();
59853
- const clip$1 = (text) => text.length <= MAX_LINE_CHARS ? text : `${text.slice(0, MAX_LINE_CHARS)}…`;
60416
+ const clip$2 = (text) => text.length <= MAX_LINE_CHARS ? text : `${text.slice(0, MAX_LINE_CHARS)}…`;
59854
60417
  /** The line an event contributes, or null when it says nothing about the work. */
59855
60418
  function describeEvent(event, implementerThreadId) {
59856
60419
  switch (event.type) {
59857
60420
  case "thread.message-sent": {
59858
60421
  if (event.payload.threadId !== implementerThreadId) return null;
59859
60422
  const text = oneLine(event.payload.text);
59860
- return text.length === 0 ? null : clip$1(`${event.payload.role}: ${text}`);
60423
+ return text.length === 0 ? null : clip$2(`${event.payload.role}: ${text}`);
59861
60424
  }
59862
60425
  case "thread.activity-appended": {
59863
60426
  if (event.payload.threadId !== implementerThreadId) return null;
59864
60427
  const activity = event.payload.activity;
59865
- return clip$1(oneLine(`${activity.kind}: ${activity.summary}`));
60428
+ return clip$2(oneLine(`${activity.kind}: ${activity.summary}`));
59866
60429
  }
59867
60430
  case "thread.turn-completed":
59868
60431
  if (event.payload.threadId !== implementerThreadId) return null;
@@ -60085,7 +60648,7 @@ function renderPrefetchHint(selected) {
60085
60648
  //#endregion
60086
60649
  //#region src/orchestration/userInputRanking.ts
60087
60650
  const MAX_OPTIONS = 255;
60088
- const MAX_QUESTION_CHARS = 16e3;
60651
+ const MAX_QUESTION_CHARS$1 = 16e3;
60089
60652
  const RANKING_TIMEOUT_MS = 1e4;
60090
60653
  const PROBABILITY_SUM_TOLERANCE = .01;
60091
60654
  /** Require a complete distribution; never invent missing option ranks. Equal scores share rank. */
@@ -60103,13 +60666,17 @@ function ranksFromAnswer(answer, count) {
60103
60666
  })).sort((a, b) => b.probability - a.probability || a.optionIndex - b.optionIndex);
60104
60667
  if (Math.abs(ranked.reduce((sum, item) => sum + item.probability, 0) - 1) > PROBABILITY_SUM_TOLERANCE) return null;
60105
60668
  let rank = 1;
60106
- return { ranks: ranked.map((item, index) => {
60107
- if (index > 0 && item.probability !== ranked[index - 1]?.probability) rank = index + 1;
60108
- return {
60109
- optionIndex: item.optionIndex,
60110
- rank
60111
- };
60112
- }) };
60669
+ return {
60670
+ confidence: answer.confidence,
60671
+ ranks: ranked.map((item, index) => {
60672
+ if (index > 0 && item.probability !== ranked[index - 1]?.probability) rank = index + 1;
60673
+ return {
60674
+ optionIndex: item.optionIndex,
60675
+ rank,
60676
+ probability: item.probability
60677
+ };
60678
+ })
60679
+ };
60113
60680
  }
60114
60681
  const makeUserInputRanker = Effect.gen(function* () {
60115
60682
  const query = yield* ProjectionSnapshotQuery;
@@ -60128,7 +60695,7 @@ const makeUserInputRanker = Effect.gen(function* () {
60128
60695
  const decoded = Schema$1.decodeUnknownOption(Schema$1.Struct({ questions: Schema$1.Array(UserInputQuestion) }))(payload);
60129
60696
  const question = Option.isSome(decoded) ? decoded.value.questions.find((question) => question.id === input.questionId) : void 0;
60130
60697
  if (!question) return yield* fail("This question is no longer pending.");
60131
- if (question.options.length < 2 || question.options.length > MAX_OPTIONS || question.question.length + question.options.reduce((sum, option) => sum + option.label.length + option.description.length, 0) > MAX_QUESTION_CHARS) return yield* fail("This question cannot be ranked.");
60698
+ if (question.options.length < 2 || question.options.length > MAX_OPTIONS || question.question.length + question.options.reduce((sum, option) => sum + option.label.length + option.description.length, 0) > MAX_QUESTION_CHARS$1) return yield* fail("This question cannot be ranked.");
60132
60699
  const context = query.getHandoffContext ? yield* query.getHandoffContext(input.threadId).pipe(Effect.mapError(() => fail("Could not read question context."))) : Option.none();
60133
60700
  if (Option.isNone(context)) return yield* fail("Question context is unavailable.");
60134
60701
  const state = context.value.transcript;
@@ -60568,8 +61135,15 @@ const FUSION_SAFETY_KERNEL = [
60568
61135
  * `task_propose` is deliberately absent: it tells its caller to stop and wait
60569
61136
  * for the user, and a supervisor that waits is one the builder sits idle
60570
61137
  * behind. Asking the user is what `ask_user_question` is for.
61138
+ *
61139
+ * `thread_spawn` is here because starting a thread is a coordination act: when
61140
+ * the authorized work needs a separate checkout, a different provider or its
61141
+ * own pair, the half that owns the plan is the half that decides. It stays in
61142
+ * the builder's denial list, so the pair still has exactly one thread that can
61143
+ * open new work.
60571
61144
  */
60572
61145
  const FUSION_SUPERVISOR_P4_TOOLS = /* @__PURE__ */ new Set([
61146
+ "thread_spawn",
60573
61147
  "thread_watch_review",
60574
61148
  "thread_watch_events",
60575
61149
  "thread_evidence_report",
@@ -60587,7 +61161,8 @@ const FUSION_SUPERVISOR_P4_TOOLS = /* @__PURE__ */ new Set([
60587
61161
  "task_list",
60588
61162
  "task_create",
60589
61163
  "task_update",
60590
- "ticket_resolve"
61164
+ "ticket_resolve",
61165
+ "ticket_comment"
60591
61166
  ]);
60592
61167
  /**
60593
61168
  * What the builder half must not reach.
@@ -60597,8 +61172,9 @@ const FUSION_SUPERVISOR_P4_TOOLS = /* @__PURE__ */ new Set([
60597
61172
  * those should reach it without an edit here. What it must never have is the
60598
61173
  * pair's own machinery - spawning threads, creating pairs, advising, answering
60599
61174
  * its own gate - the phase plan, which the supervisor owns outright, plus
60600
- * task-board mutation. Both roles may read paired history and task-relevant
60601
- * external evidence; only Supervisor writes review reports and the phase plan.
61175
+ * task-board mutation and the tracker comment that closes a task. Both roles
61176
+ * may read paired history and task-relevant external evidence; only Supervisor
61177
+ * writes review reports and the phase plan.
60602
61178
  *
60603
61179
  * Both roles retain `thread_pair_detach` as their way out. Its handler limits
60604
61180
  * detaching to a pair containing the caller whose other thread this session
@@ -60620,6 +61196,7 @@ const FUSION_BUILDER_DENIED_P4_TOOLS = /* @__PURE__ */ new Set([
60620
61196
  "task_create",
60621
61197
  "task_update",
60622
61198
  "task_propose",
61199
+ "ticket_comment",
60623
61200
  "memory_append"
60624
61201
  ]);
60625
61202
  /**
@@ -60818,7 +61395,7 @@ const forceStopOwnedProcess = (threadId, options) => threadLocks.withPermit(thre
60818
61395
  }));
60819
61396
  //#endregion
60820
61397
  //#region ../../packages/shared/src/toolActivity.ts
60821
- function asRecord$7(value) {
61398
+ function asRecord$9(value) {
60822
61399
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
60823
61400
  }
60824
61401
  function asTrimmedString$1(value) {
@@ -60848,10 +61425,10 @@ function extractCommandFromTitle$1(title) {
60848
61425
  return /`([^`]+)`/u.exec(title)?.[1]?.trim() || void 0;
60849
61426
  }
60850
61427
  function extractToolCommand(data, title) {
60851
- const item = asRecord$7(data?.item);
60852
- const itemInput = asRecord$7(item?.input);
60853
- const itemResult = asRecord$7(item?.result);
60854
- const rawInput = asRecord$7(data?.rawInput);
61428
+ const item = asRecord$9(data?.item);
61429
+ const itemInput = asRecord$9(item?.input);
61430
+ const itemResult = asRecord$9(item?.result);
61431
+ const rawInput = asRecord$9(data?.rawInput);
60855
61432
  const direct = [
60856
61433
  normalizeCommandValue$1(item?.command),
60857
61434
  normalizeCommandValue$1(itemInput?.command),
@@ -60879,7 +61456,7 @@ function collectPaths(value, paths, seen, depth) {
60879
61456
  }
60880
61457
  return;
60881
61458
  }
60882
- const record = asRecord$7(value);
61459
+ const record = asRecord$9(value);
60883
61460
  if (!record) return;
60884
61461
  for (const key of [
60885
61462
  "path",
@@ -60938,7 +61515,7 @@ function deriveToolActivityPresentation(input) {
60938
61515
  const title = asTrimmedString$1(input.title);
60939
61516
  const detail = stripTrailingExitCode(asTrimmedString$1(input.detail));
60940
61517
  const fallbackSummary = asTrimmedString$1(input.fallbackSummary) ?? "Tool";
60941
- const data = asRecord$7(input.data);
61518
+ const data = asRecord$9(input.data);
60942
61519
  const command = extractToolCommand(data, title);
60943
61520
  const primaryPath = extractPrimaryPath(data);
60944
61521
  const action = classifyToolAction({
@@ -60962,7 +61539,7 @@ function deriveToolActivityPresentation(input) {
60962
61539
  ...primaryPath ? { detail: primaryPath } : {}
60963
61540
  };
60964
61541
  if (action === "search") {
60965
- const query = asTrimmedString$1(asRecord$7(data?.rawInput)?.query) ?? asTrimmedString$1(asRecord$7(data?.rawInput)?.pattern) ?? asTrimmedString$1(asRecord$7(data?.rawInput)?.searchTerm);
61542
+ const query = asTrimmedString$1(asRecord$9(data?.rawInput)?.query) ?? asTrimmedString$1(asRecord$9(data?.rawInput)?.pattern) ?? asTrimmedString$1(asRecord$9(data?.rawInput)?.searchTerm);
60966
61543
  return {
60967
61544
  summary: "Searched files",
60968
61545
  ...query ? { detail: query } : {}
@@ -63215,6 +63792,7 @@ const P4CODE_MCP_TOOL_NAMES = [
63215
63792
  "thread_spawn",
63216
63793
  "thread_watch_events",
63217
63794
  "thread_watch_review",
63795
+ "ticket_comment",
63218
63796
  "ticket_resolve"
63219
63797
  ];
63220
63798
  /** The catalogue as the adapters read it. */
@@ -98870,7 +99448,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
98870
99448
  };
98871
99449
  });
98872
99450
  var OpenCodeRuntime = class extends Context.Service()("@p4code/cli/provider/opencodeRuntime") {};
98873
- const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$122));
99451
+ const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$123));
98874
99452
  //#endregion
98875
99453
  //#region src/textGeneration/OpenCodeTextGeneration.ts
98876
99454
  const OPENCODE_TEXT_GENERATION_IDLE_TTL = "30 seconds";
@@ -103776,6 +104354,136 @@ const openForActiveThread = Effect.fn("DeviceThreadAdmission.open")(function* (i
103776
104354
  function isRegisteredProviderWorkspace(cwd, projects, threads) {
103777
104355
  return projects.some((project) => project.workspaceRoot === cwd) || threads.some((thread) => thread.worktreePath === cwd);
103778
104356
  }
104357
+ //#endregion
104358
+ //#region src/hub/HubActivityTransport.ts
104359
+ const REQUEST_TIMEOUT_MS$3 = 8e3;
104360
+ const CLEANUP_TIMEOUT_MS = 1e3;
104361
+ const MAX_RESPONSE_BYTES = 4096;
104362
+ const MAX_REQUEST_BYTES = 16 * 1024;
104363
+ const MAX_BEARER_CHARS = 8192;
104364
+ var HubActivityUnavailable = class extends Data.TaggedError("HubActivityUnavailable") {};
104365
+ var HubActivityUnauthorized = class extends Data.TaggedError("HubActivityUnauthorized") {};
104366
+ /** All errors are payload-free; late responses still cancel their bodies after owner interruption. */
104367
+ const makeHubActivityTransport = Effect.fn("HubActivityTransport.make")(function* (settings, fetcher = fetch) {
104368
+ const origin = yield* Effect.try({
104369
+ try: () => {
104370
+ const url = new URL(settings.baseUrl);
104371
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/" || settings.token.length === 0 || settings.token.length > MAX_BEARER_CHARS || !/^[\x21-\x7e]+$/.test(settings.token)) throw new HubActivityUnavailable();
104372
+ return url.origin;
104373
+ },
104374
+ catch: () => new HubActivityUnavailable()
104375
+ });
104376
+ const request = Effect.fn("HubActivityTransport.request")(function* (path, body, membership) {
104377
+ return yield* Effect.tryPromise({
104378
+ try: async (signal) => {
104379
+ const controller = new AbortController();
104380
+ const abort = () => controller.abort();
104381
+ signal.addEventListener("abort", abort, { once: true });
104382
+ if (signal.aborted) abort();
104383
+ const timer = setTimeout(abort, REQUEST_TIMEOUT_MS$3);
104384
+ let reader;
104385
+ const cancel = async (work) => {
104386
+ let timeout;
104387
+ try {
104388
+ await Promise.race([Promise.resolve().then(work).catch(() => void 0), new Promise((resolve) => {
104389
+ timeout = setTimeout(resolve, CLEANUP_TIMEOUT_MS);
104390
+ })]);
104391
+ } finally {
104392
+ if (timeout) clearTimeout(timeout);
104393
+ }
104394
+ };
104395
+ try {
104396
+ if (controller.signal.aborted) throw new HubActivityUnavailable();
104397
+ const response = await fetcher(`${origin}${path}`, {
104398
+ method: body === void 0 ? "GET" : "POST",
104399
+ headers: {
104400
+ authorization: `Bearer ${settings.token}`,
104401
+ ...membership ? {
104402
+ "x-p4-hub-account": membership.accountId,
104403
+ "x-p4-hub-environment": membership.environmentId
104404
+ } : {},
104405
+ ...body === void 0 ? {} : { "content-type": "application/json" }
104406
+ },
104407
+ body,
104408
+ signal: controller.signal,
104409
+ redirect: "error",
104410
+ credentials: "omit",
104411
+ cache: "no-store"
104412
+ });
104413
+ if (controller.signal.aborted) {
104414
+ await cancel(() => response.body?.cancel() ?? Promise.resolve());
104415
+ throw new HubActivityUnavailable();
104416
+ }
104417
+ reader = response.body?.getReader();
104418
+ if (response.status === 401 || response.status === 403) throw new HubActivityUnauthorized();
104419
+ if (response.status !== 200 || !/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") ?? "") || !reader) throw new HubActivityUnavailable();
104420
+ let length = 0;
104421
+ const chunks = [];
104422
+ while (true) {
104423
+ const item = await reader.read();
104424
+ if (controller.signal.aborted) throw new HubActivityUnavailable();
104425
+ if (item.done) break;
104426
+ length += item.value.byteLength;
104427
+ if (length > MAX_RESPONSE_BYTES) throw new HubActivityUnavailable();
104428
+ if (item.value.byteLength) chunks.push(item.value);
104429
+ }
104430
+ const bytes = new Uint8Array(length);
104431
+ let offset = 0;
104432
+ for (const chunk of chunks) {
104433
+ bytes.set(chunk, offset);
104434
+ offset += chunk.byteLength;
104435
+ }
104436
+ return Schema$1.decodeUnknownSync(Schema$1.UnknownFromJsonString)(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
104437
+ } finally {
104438
+ controller.abort();
104439
+ clearTimeout(timer);
104440
+ signal.removeEventListener("abort", abort);
104441
+ if (reader) await cancel(() => reader.cancel());
104442
+ }
104443
+ },
104444
+ catch: (error) => error instanceof HubActivityUnauthorized ? error : new HubActivityUnavailable()
104445
+ }).pipe(Effect.timeout(9e3), Effect.mapError((error) => error instanceof HubActivityUnauthorized ? error : new HubActivityUnavailable()));
104446
+ });
104447
+ return {
104448
+ identity: Effect.fn("HubActivityTransport.identity")(function* () {
104449
+ const membership = yield* request("/v1/identity").pipe(Effect.flatMap(Schema$1.decodeUnknownEffect(HubServerMembership, { onExcessProperty: "error" })), Effect.mapError((error) => error instanceof HubActivityUnauthorized ? error : new HubActivityUnavailable()));
104450
+ const destination = yield* Schema$1.encodeUnknownEffect(Schema$1.UnknownFromJsonString)([
104451
+ origin,
104452
+ membership.accountId,
104453
+ membership.environmentId
104454
+ ]);
104455
+ return {
104456
+ ...membership,
104457
+ destination: NodeCrypto.createHash("sha256").update(destination).digest("hex")
104458
+ };
104459
+ }),
104460
+ post: Effect.fn("HubActivityTransport.post")(function* (input, membership) {
104461
+ const activity = yield* Schema$1.decodeUnknownEffect(HubAgentActivityInput)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => new HubActivityUnavailable()));
104462
+ const body = yield* Schema$1.encodeEffect(Schema$1.fromJsonString(HubAgentActivityInput))(activity);
104463
+ if (Buffer.byteLength(body) > MAX_REQUEST_BYTES) return yield* Effect.fail(new HubActivityUnavailable());
104464
+ const result = yield* request("/v1/agent-activity", body, membership);
104465
+ return yield* Schema$1.decodeUnknownEffect(Schema$1.Struct({ accepted: Schema$1.Boolean }))(result, { onExcessProperty: "error" }).pipe(Effect.mapError(() => new HubActivityUnavailable()));
104466
+ }),
104467
+ postFact: Effect.fn("HubActivityTransport.postFact")(function* (input, membership) {
104468
+ const fact = yield* Schema$1.decodeUnknownEffect(HubAnalyticsFactInput)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => new HubActivityUnavailable()));
104469
+ const body = yield* Schema$1.encodeEffect(Schema$1.fromJsonString(HubAnalyticsFactInput))(fact);
104470
+ if (Buffer.byteLength(body) > MAX_REQUEST_BYTES) return yield* Effect.fail(new HubActivityUnavailable());
104471
+ const result = yield* request("/v1/analytics/facts", body, membership);
104472
+ return yield* Schema$1.decodeUnknownEffect(Schema$1.Struct({ accepted: Schema$1.Boolean }))(result, { onExcessProperty: "error" }).pipe(Effect.mapError(() => new HubActivityUnavailable()));
104473
+ }),
104474
+ queryFacts: Effect.fn("HubActivityTransport.queryFacts")(function* (query, membership) {
104475
+ const search = new URLSearchParams();
104476
+ for (const kind of query.kinds ?? []) search.append("kind", kind);
104477
+ if (query.since !== void 0) search.set("since", query.since);
104478
+ if (query.until !== void 0) search.set("until", query.until);
104479
+ if (query.limit !== void 0) search.set("limit", String(query.limit));
104480
+ if (query.cursor !== void 0) search.set("cursor", query.cursor);
104481
+ const suffix = search.size === 0 ? "" : `?${search.toString()}`;
104482
+ const result = yield* request(`/v1/analytics/facts${suffix}`, void 0, membership);
104483
+ return yield* Schema$1.decodeUnknownEffect(HubAnalyticsPage)(result, { onExcessProperty: "error" }).pipe(Effect.mapError(() => new HubActivityUnavailable()));
104484
+ })
104485
+ };
104486
+ });
103779
104487
  (() => {
103780
104488
  try {
103781
104489
  return process.env.NODE_ENV === "development";
@@ -104486,7 +105194,7 @@ const NESTED_PAYLOAD_KEYS = [
104486
105194
  "operations"
104487
105195
  ];
104488
105196
  const MAX_COLLECT_DEPTH = 4;
104489
- function asRecord$6(value) {
105197
+ function asRecord$8(value) {
104490
105198
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
104491
105199
  }
104492
105200
  function pushChangedFilePath(target, value) {
@@ -104501,7 +105209,7 @@ function collectChangedFilePaths(value, target, depth) {
104501
105209
  for (const entry of value) collectChangedFilePaths(entry, target, depth + 1);
104502
105210
  return;
104503
105211
  }
104504
- const record = asRecord$6(value);
105212
+ const record = asRecord$8(value);
104505
105213
  if (!record) return;
104506
105214
  for (const field of CHANGED_FILE_FIELDS) pushChangedFilePath(target, record[field]);
104507
105215
  for (const nestedKey of NESTED_PAYLOAD_KEYS) if (nestedKey in record) collectChangedFilePaths(record[nestedKey], target, depth + 1);
@@ -104512,7 +105220,7 @@ function collectChangedFilePaths(value, target, depth) {
104512
105220
  */
104513
105221
  function collectActivityChangedFilePaths(payload) {
104514
105222
  const target = /* @__PURE__ */ new Set();
104515
- collectChangedFilePaths(asRecord$6(asRecord$6(payload)?.data), target, 0);
105223
+ collectChangedFilePaths(asRecord$8(asRecord$8(payload)?.data), target, 0);
104516
105224
  return target;
104517
105225
  }
104518
105226
  /**
@@ -122485,7 +123193,7 @@ const makeBtwRequestCoordinator = (dependencies) => Effect.gen(function* () {
122485
123193
  * already negotiated.
122486
123194
  */
122487
123195
  const MCP_PROTOCOL_VERSION = "2025-06-18";
122488
- const REQUEST_TIMEOUT_MS$3 = 15e3;
123196
+ const REQUEST_TIMEOUT_MS$2 = 15e3;
122489
123197
  var McpToolCallError = class extends Schema$1.TaggedErrorClass()("McpToolCallError", {
122490
123198
  detail: Schema$1.String,
122491
123199
  status: Schema$1.NullOr(Schema$1.Number)
@@ -122549,7 +123257,7 @@ const callMcpTool = Effect.fn("McpToolClient.callMcpTool")(function* (input) {
122549
123257
  ...input.headers,
122550
123258
  ...extraHeaders,
122551
123259
  accept: "application/json, text/event-stream"
122552
- }), body)).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$3), Effect.catchCause((cause) => Effect.fail(new McpToolCallError({
123260
+ }), body)).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$2), Effect.catchCause((cause) => Effect.fail(new McpToolCallError({
122553
123261
  detail: `Could not reach ${input.url}: ${String(cause)}`,
122554
123262
  status: null
122555
123263
  }))));
@@ -122630,6 +123338,9 @@ const callMcpTool = Effect.fn("McpToolClient.callMcpTool")(function* (input) {
122630
123338
  * name that drifts between them fails at runtime with a shrug from the server.
122631
123339
  */
122632
123340
  const LINEAR_GET_ISSUE_TOOL = "get_issue";
123341
+ /** The write half: one comment, and the links filed beside it. */
123342
+ const LINEAR_SAVE_COMMENT_TOOL = "save_comment";
123343
+ const LINEAR_CREATE_ATTACHMENT_TOOL = "create_attachment";
122633
123344
  /** Why a Linear call could not be made, in terms a person can act on. */
122634
123345
  var LinearUnavailable = class extends Schema$1.TaggedErrorClass()("LinearUnavailable", {
122635
123346
  reason: Schema$1.Literals([
@@ -122643,7 +123354,7 @@ var LinearUnavailable = class extends Schema$1.TaggedErrorClass()("LinearUnavail
122643
123354
  return this.detail;
122644
123355
  }
122645
123356
  };
122646
- const asRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
123357
+ const asRecord$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
122647
123358
  const asArray$1 = (value) => Array.isArray(value) ? value : void 0;
122648
123359
  /**
122649
123360
  * Find the payload inside whatever the tool returned.
@@ -122673,22 +123384,22 @@ const readPayload = (result) => {
122673
123384
  */
122674
123385
  const readRows = (payload) => {
122675
123386
  const direct = asArray$1(payload);
122676
- if (direct !== void 0) return direct.map(asRecord$5).filter((row) => row !== void 0);
122677
- const record = asRecord$5(payload);
123387
+ if (direct !== void 0) return direct.map(asRecord$7).filter((row) => row !== void 0);
123388
+ const record = asRecord$7(payload);
122678
123389
  if (record === void 0) return [];
122679
123390
  for (const value of Object.values(record)) {
122680
123391
  const rows = asArray$1(value);
122681
- if (rows !== void 0) return rows.map(asRecord$5).filter((row) => row !== void 0);
123392
+ if (rows !== void 0) return rows.map(asRecord$7).filter((row) => row !== void 0);
122682
123393
  }
122683
123394
  return [];
122684
123395
  };
122685
123396
  /** The single object out of a result, unwrapping one level of nesting. */
122686
123397
  const readOne = (payload) => {
122687
- const record = asRecord$5(payload);
123398
+ const record = asRecord$7(payload);
122688
123399
  if (record === void 0) return;
122689
123400
  if (record["id"] !== void 0 || record["identifier"] !== void 0) return record;
122690
123401
  for (const value of Object.values(record)) {
122691
- const nested = asRecord$5(value);
123402
+ const nested = asRecord$7(value);
122692
123403
  if (nested?.["id"] !== void 0) return nested;
122693
123404
  }
122694
123405
  };
@@ -122798,7 +123509,7 @@ const TrackerSecretsLive = Layer.effect(TrackerSecrets, makeTrackerSecrets);
122798
123509
  *
122799
123510
  * @module tracker/graphqlRequest
122800
123511
  */
122801
- const REQUEST_TIMEOUT_MS$2 = 15e3;
123512
+ const REQUEST_TIMEOUT_MS$1 = 15e3;
122802
123513
  var GraphqlRequestError = class extends Schema$1.TaggedErrorClass()("GraphqlRequestError", {
122803
123514
  detail: Schema$1.String,
122804
123515
  status: Schema$1.NullOr(Schema$1.Number)
@@ -122807,14 +123518,14 @@ var GraphqlRequestError = class extends Schema$1.TaggedErrorClass()("GraphqlRequ
122807
123518
  return this.detail;
122808
123519
  }
122809
123520
  };
122810
- const asRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
123521
+ const asRecord$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
122811
123522
  const errorMessages = (value) => {
122812
123523
  if (!Array.isArray(value)) return [];
122813
- return value.map((entry) => asRecord$4(entry)?.["message"]).filter((message) => typeof message === "string");
123524
+ return value.map((entry) => asRecord$6(entry)?.["message"]).filter((message) => typeof message === "string");
122814
123525
  };
122815
123526
  const parseJsonRecord = (text) => {
122816
123527
  try {
122817
- return asRecord$4(JSON.parse(text));
123528
+ return asRecord$6(JSON.parse(text));
122818
123529
  } catch {
122819
123530
  return;
122820
123531
  }
@@ -122823,7 +123534,7 @@ const graphqlRequest = Effect.fn("tracker/graphqlRequest")(function* (input) {
122823
123534
  const response = yield* (yield* HttpClient$1.HttpClient).execute(HttpClientRequest$1.bodyJsonUnsafe(HttpClientRequest$1.setHeaders(HttpClientRequest$1.post(input.url), input.headers), {
122824
123535
  query: input.query,
122825
123536
  variables: input.variables ?? {}
122826
- })).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$2), Effect.catchCause((cause) => Effect.fail(new GraphqlRequestError({
123537
+ })).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$1), Effect.catchCause((cause) => Effect.fail(new GraphqlRequestError({
122827
123538
  detail: `Could not reach ${input.url}: ${String(cause)}`,
122828
123539
  status: null
122829
123540
  }))));
@@ -122838,7 +123549,7 @@ const graphqlRequest = Effect.fn("tracker/graphqlRequest")(function* (input) {
122838
123549
  status: response.status
122839
123550
  });
122840
123551
  return {
122841
- data: asRecord$4(record["data"]),
123552
+ data: asRecord$6(record["data"]),
122842
123553
  errors: errorMessages(record["errors"])
122843
123554
  };
122844
123555
  });
@@ -122847,10 +123558,11 @@ const graphqlRequest = Effect.fn("tracker/graphqlRequest")(function* (input) {
122847
123558
  /**
122848
123559
  * Linear over its own GraphQL API, wearing the MCP tool vocabulary.
122849
123560
  *
122850
- * This speaks the same three "tools" the Linear MCP server offers -
122851
- * `list_issues`, `get_issue`, `save_issue` - and answers in the same row
122852
- * shapes, so `LinearTasks` and the resolver cannot tell which transport
122853
- * served them. That is the point: the MCP server remains the fallback, and a
123561
+ * This speaks the same "tools" the Linear MCP server offers - `list_issues`,
123562
+ * `get_issue`, `save_issue`, `list_comments`, `save_comment` and
123563
+ * `create_attachment` - and answers in the same row shapes, so `LinearTasks`,
123564
+ * the resolver and the closeout path cannot tell which transport served
123565
+ * them. That is the point: the MCP server remains the fallback, and a
122854
123566
  * consumer that could tell the two apart would need two code paths.
122855
123567
  *
122856
123568
  * The one real difference is hidden here: the MCP tools accept names where
@@ -122891,7 +123603,7 @@ const STATE_TYPES = /* @__PURE__ */ new Set([
122891
123603
  "completed",
122892
123604
  "canceled"
122893
123605
  ]);
122894
- const asRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
123606
+ const asRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
122895
123607
  const asArray = (value) => Array.isArray(value) ? value : [];
122896
123608
  const asString = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
122897
123609
  /**
@@ -122901,9 +123613,9 @@ const asString = (value) => typeof value === "string" && value.trim().length > 0
122901
123613
  * `labels` stay structured - the mapping already accepts both forms.
122902
123614
  */
122903
123615
  const toIssueRow = (node) => {
122904
- const state = asRecord$3(node["state"]);
122905
- const parent = asRecord$3(node["parent"]);
122906
- const labels = asRecord$3(node["labels"]);
123616
+ const state = asRecord$5(node["state"]);
123617
+ const parent = asRecord$5(node["parent"]);
123618
+ const labels = asRecord$5(node["labels"]);
122907
123619
  return {
122908
123620
  id: node["id"],
122909
123621
  identifier: node["identifier"],
@@ -122920,6 +123632,29 @@ const toIssueRow = (node) => {
122920
123632
  parentId: parent?.["id"]
122921
123633
  };
122922
123634
  };
123635
+ /** Everything a closeout comment's caller renders back: who, when, and where. */
123636
+ const COMMENT_SELECTION = `
123637
+ id
123638
+ body
123639
+ url
123640
+ createdAt
123641
+ user { id name displayName }
123642
+ `;
123643
+ /** An attachment as Linear files it: a link with a label, against one issue. */
123644
+ const ATTACHMENT_SELECTION = `
123645
+ id
123646
+ url
123647
+ title
123648
+ subtitle
123649
+ `;
123650
+ /** One comment node, flattened the way `toIssueRow` flattens an issue. */
123651
+ const toCommentRow = (node) => ({
123652
+ id: node["id"],
123653
+ body: node["body"],
123654
+ url: node["url"],
123655
+ createdAt: node["createdAt"],
123656
+ user: node["user"]
123657
+ });
122923
123658
  /** A case-insensitive equality filter over the ways a person is named. */
122924
123659
  const personFilter = (person) => ({ or: [
122925
123660
  { displayName: { eqIgnoreCase: person } },
@@ -122943,7 +123678,7 @@ const makeLinearApiTransport = Effect.gen(function* () {
122943
123678
  })));
122944
123679
  /** The one entity out of `data`, or a failure carrying Linear's own words. */
122945
123680
  const readEntity = (response, key) => {
122946
- const entity = asRecord$3(response.data?.[key]);
123681
+ const entity = asRecord$5(response.data?.[key]);
122947
123682
  if (entity !== void 0) return Effect.succeed(entity);
122948
123683
  if (response.errors.length === 0) return Effect.void.pipe(Effect.as(void 0));
122949
123684
  if (response.errors.some((message) => /not found|does not exist/iu.test(message))) return Effect.void.pipe(Effect.as(void 0));
@@ -122952,7 +123687,7 @@ const makeLinearApiTransport = Effect.gen(function* () {
122952
123687
  detail: response.errors.join("; ")
122953
123688
  }));
122954
123689
  };
122955
- const nodesOf = (response, key) => asArray(asRecord$3(response.data?.[key])?.["nodes"]).map(asRecord$3).filter((node) => node !== void 0);
123690
+ const nodesOf = (response, key) => asArray(asRecord$5(response.data?.[key])?.["nodes"]).map(asRecord$5).filter((node) => node !== void 0);
122956
123691
  const resolveTeamId = (apiKey, team) => Effect.gen(function* () {
122957
123692
  const response = yield* request(apiKey, `query($filter: TeamFilter) { teams(filter: $filter, first: 2) { nodes { id } } }`, { filter: { or: [{ key: { eqIgnoreCase: team } }, { name: { eqIgnoreCase: team } }] } });
122958
123693
  const id = asString(nodesOf(response, "teams")[0]?.["id"]);
@@ -122965,7 +123700,7 @@ const makeLinearApiTransport = Effect.gen(function* () {
122965
123700
  const teamIdOfIssue = (apiKey, issueId) => Effect.gen(function* () {
122966
123701
  const response = yield* request(apiKey, `query($id: String!) { issue(id: $id) { team { id } } }`, { id: issueId });
122967
123702
  const issue = yield* readEntity(response, "issue");
122968
- const id = asString(asRecord$3(issue?.["team"])?.["id"]);
123703
+ const id = asString(asRecord$5(issue?.["team"])?.["id"]);
122969
123704
  if (id === void 0) return yield* new LinearUnavailable({
122970
123705
  reason: "failed",
122971
123706
  detail: `Linear has no issue "${issueId}" to read a team from.`
@@ -123085,13 +123820,114 @@ const makeLinearApiTransport = Effect.gen(function* () {
123085
123820
  input
123086
123821
  });
123087
123822
  const payload = yield* readEntity(response, issueId === void 0 ? "issueCreate" : "issueUpdate");
123088
- const issue = asRecord$3(payload?.["issue"]);
123823
+ const issue = asRecord$5(payload?.["issue"]);
123089
123824
  if (issue === void 0) return yield* new LinearUnavailable({
123090
123825
  reason: "failed",
123091
123826
  detail: response.errors.length > 0 ? response.errors.join("; ") : "Linear accepted the write but returned no issue."
123092
123827
  });
123093
123828
  return toIssueRow(issue);
123094
123829
  });
123830
+ /**
123831
+ * The issue's own id, from whatever a caller had - an identifier, a URL's
123832
+ * tail, or the id itself.
123833
+ *
123834
+ * The comment and attachment mutations take the UUID only, while every
123835
+ * caller here starts from what a person typed. One lookup closes that gap
123836
+ * and gives the "no such issue" sentence a name to report.
123837
+ */
123838
+ const resolveIssueNodeId = (apiKey, reference) => Effect.gen(function* () {
123839
+ const response = yield* request(apiKey, `query($id: String!) { issue(id: $id) { id } }`, { id: reference });
123840
+ const issue = yield* readEntity(response, "issue");
123841
+ const id = asString(issue?.["id"]);
123842
+ if (id === void 0) return yield* new LinearUnavailable({
123843
+ reason: "failed",
123844
+ detail: `Linear has no issue "${reference}".`
123845
+ });
123846
+ return id;
123847
+ });
123848
+ const listComments = (apiKey, args) => Effect.gen(function* () {
123849
+ const reference = asString(args["issueId"]) ?? asString(args["id"]);
123850
+ if (reference === void 0) return yield* new LinearUnavailable({
123851
+ reason: "failed",
123852
+ detail: "list_comments needs an issueId."
123853
+ });
123854
+ const response = yield* request(apiKey, `query($id: String!, $first: Int!) {
123855
+ issue(id: $id) {
123856
+ comments(first: $first) {
123857
+ nodes { ${COMMENT_SELECTION} }
123858
+ }
123859
+ }
123860
+ }`, {
123861
+ id: reference,
123862
+ first: typeof args["limit"] === "number" ? args["limit"] : 50
123863
+ });
123864
+ const issue = yield* readEntity(response, "issue");
123865
+ if (issue === void 0) return yield* new LinearUnavailable({
123866
+ reason: "failed",
123867
+ detail: `Linear has no issue "${reference}".`
123868
+ });
123869
+ return { comments: asArray(asRecord$5(issue["comments"])?.["nodes"]).map(asRecord$5).filter((node) => node !== void 0).map(toCommentRow) };
123870
+ });
123871
+ const saveComment = (apiKey, args) => Effect.gen(function* () {
123872
+ const reference = asString(args["issueId"]) ?? asString(args["id"]);
123873
+ const body = typeof args["body"] === "string" ? args["body"] : void 0;
123874
+ if (reference === void 0 || body === void 0) return yield* new LinearUnavailable({
123875
+ reason: "failed",
123876
+ detail: "save_comment needs an issueId and a body."
123877
+ });
123878
+ const issueNodeId = yield* resolveIssueNodeId(apiKey, reference);
123879
+ const parentId = asString(args["parentId"]);
123880
+ const response = yield* request(apiKey, `mutation($input: CommentCreateInput!) {
123881
+ commentCreate(input: $input) { comment { ${COMMENT_SELECTION} } }
123882
+ }`, { input: {
123883
+ issueId: issueNodeId,
123884
+ body,
123885
+ ...parentId === void 0 ? {} : { parentId }
123886
+ } });
123887
+ const payload = yield* readEntity(response, "commentCreate");
123888
+ const comment = asRecord$5(payload?.["comment"]);
123889
+ if (comment === void 0) return yield* new LinearUnavailable({
123890
+ reason: "failed",
123891
+ detail: response.errors.length > 0 ? response.errors.join("; ") : "Linear accepted the comment and returned nothing."
123892
+ });
123893
+ return toCommentRow(comment);
123894
+ });
123895
+ /**
123896
+ * A link filed against an issue, which is every attachment this transport
123897
+ * can make.
123898
+ *
123899
+ * Linear's own file upload is a signed-URL handshake followed by a PUT of
123900
+ * the bytes, and nothing here has bytes to send: the proof a closeout links
123901
+ * is already hosted somewhere. A caller holding a local file gets the same
123902
+ * answer it would get from the MCP server's `create_attachment`, which also
123903
+ * takes a URL.
123904
+ */
123905
+ const createAttachment = (apiKey, args) => Effect.gen(function* () {
123906
+ const reference = asString(args["issueId"]) ?? asString(args["id"]);
123907
+ const url = asString(args["url"]);
123908
+ if (reference === void 0 || url === void 0) return yield* new LinearUnavailable({
123909
+ reason: "failed",
123910
+ detail: "create_attachment needs an issueId and a url."
123911
+ });
123912
+ const issueNodeId = yield* resolveIssueNodeId(apiKey, reference);
123913
+ const title = asString(args["title"]);
123914
+ const subtitle = asString(args["subtitle"]);
123915
+ const response = yield* request(apiKey, `mutation($input: AttachmentCreateInput!) {
123916
+ attachmentCreate(input: $input) { attachment { ${ATTACHMENT_SELECTION} } }
123917
+ }`, { input: {
123918
+ issueId: issueNodeId,
123919
+ url,
123920
+ title: title ?? url,
123921
+ ...subtitle === void 0 ? {} : { subtitle }
123922
+ } });
123923
+ const payload = yield* readEntity(response, "attachmentCreate");
123924
+ const attachment = asRecord$5(payload?.["attachment"]);
123925
+ if (attachment === void 0) return yield* new LinearUnavailable({
123926
+ reason: "failed",
123927
+ detail: response.errors.length > 0 ? response.errors.join("; ") : "Linear accepted the attachment and returned nothing."
123928
+ });
123929
+ return attachment;
123930
+ });
123095
123931
  const isConfigured = secrets.read("linear").pipe(Effect.map((credential) => Option.isSome(credential)));
123096
123932
  const call = (toolName, args) => Effect.gen(function* () {
123097
123933
  const credential = yield* secrets.read("linear");
@@ -123104,6 +123940,9 @@ const makeLinearApiTransport = Effect.gen(function* () {
123104
123940
  case "list_issues": return yield* listIssues(apiKey, args);
123105
123941
  case "get_issue": return yield* getIssue(apiKey, args);
123106
123942
  case "save_issue": return yield* saveIssue(apiKey, args);
123943
+ case "list_comments": return yield* listComments(apiKey, args);
123944
+ case "save_comment": return yield* saveComment(apiKey, args);
123945
+ case "create_attachment": return yield* createAttachment(apiKey, args);
123107
123946
  default: return yield* new LinearUnavailable({
123108
123947
  reason: "failed",
123109
123948
  detail: `The Linear API transport has no translation for "${toolName}".`
@@ -123115,11 +123954,11 @@ const makeLinearApiTransport = Effect.gen(function* () {
123115
123954
  reason: "not_authorized",
123116
123955
  detail: "Linear rejected this API key."
123117
123956
  }) : error));
123118
- if (asString(asRecord$3(response.data?.["viewer"])?.["id"]) === void 0) return yield* new LinearUnavailable({
123957
+ if (asString(asRecord$5(response.data?.["viewer"])?.["id"]) === void 0) return yield* new LinearUnavailable({
123119
123958
  reason: "not_authorized",
123120
123959
  detail: response.errors.length > 0 ? response.errors.join("; ") : "Linear rejected this API key."
123121
123960
  });
123122
- const organization = asRecord$3(response.data?.["organization"]);
123961
+ const organization = asRecord$5(response.data?.["organization"]);
123123
123962
  return { workspace: asString(organization?.["name"]) ?? asString(organization?.["urlKey"]) ?? null };
123124
123963
  });
123125
123964
  return {
@@ -123340,7 +124179,7 @@ const STATUS_FROM_LINEAR_TYPE = {
123340
124179
  completed: "done",
123341
124180
  canceled: "cancelled"
123342
124181
  };
123343
- const asRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
124182
+ const asRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
123344
124183
  const text = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
123345
124184
  /**
123346
124185
  * A person's name out of whatever Linear put in the field: an object when the
@@ -123349,14 +124188,14 @@ const text = (value) => typeof value === "string" && value.trim().length > 0 ? v
123349
124188
  const personName = (value) => {
123350
124189
  const direct = text(value);
123351
124190
  if (direct !== void 0) return direct;
123352
- const record = asRecord$2(value);
124191
+ const record = asRecord$4(value);
123353
124192
  if (record === void 0) return null;
123354
124193
  return text(record["displayName"]) ?? text(record["name"]) ?? text(record["email"]) ?? null;
123355
124194
  };
123356
124195
  /** Label names, from either `["Bug"]` or `[{ name: "Bug" }]`. */
123357
124196
  const labelNames = (value) => {
123358
124197
  if (!Array.isArray(value)) return [];
123359
- return value.map((entry) => text(entry) ?? text(asRecord$2(entry)?.["name"])).filter((name) => name !== void 0);
124198
+ return value.map((entry) => text(entry) ?? text(asRecord$4(entry)?.["name"])).filter((name) => name !== void 0);
123360
124199
  };
123361
124200
  /**
123362
124201
  * The p4code status an issue is in.
@@ -123401,7 +124240,7 @@ const statusToLinearState = (status) => {
123401
124240
  * Medium, and quietly rewrite the local row with it.
123402
124241
  */
123403
124242
  const priorityFromLinear = (value) => {
123404
- const numeric = typeof value === "number" ? value : asRecord$2(value)?.["value"];
124243
+ const numeric = typeof value === "number" ? value : asRecord$4(value)?.["value"];
123405
124244
  return typeof numeric === "number" ? PRIORITY_FROM_LINEAR[numeric] ?? "none" : "none";
123406
124245
  };
123407
124246
  const priorityToLinear = (priority) => PRIORITY_TO_LINEAR[priority];
@@ -126920,7 +127759,7 @@ function toAssetFilePath(repositoryPath, root) {
126920
127759
  const REGISTRY_SEARCH_URL = "https://www.skills.sh/api/search";
126921
127760
  const GITHUB_API_URL = "https://api.github.com";
126922
127761
  const GITHUB_RAW_URL = "https://raw.githubusercontent.com";
126923
- const REQUEST_TIMEOUT_MS$1 = 15e3;
127762
+ const REQUEST_TIMEOUT_MS = 15e3;
126924
127763
  /** More than a person reads, few enough that the panel is a list and not a feed. */
126925
127764
  const MAX_SEARCH_RESULTS = 25;
126926
127765
  /**
@@ -126960,7 +127799,7 @@ const emptyFetch = (id, unavailable) => ({
126960
127799
  const make$29 = Effect.gen(function* () {
126961
127800
  const http = yield* HttpClient.HttpClient;
126962
127801
  const request = Effect.fn("SkillRegistry.request")(function* (url) {
126963
- return yield* http.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.setHeader("accept", "application/json"), HttpClientRequest.setHeader("user-agent", "p4code"))).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$1));
127802
+ return yield* http.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.setHeader("accept", "application/json"), HttpClientRequest.setHeader("user-agent", "p4code"))).pipe(Effect.timeout(REQUEST_TIMEOUT_MS));
126964
127803
  });
126965
127804
  const search = (query) => Effect.gen(function* () {
126966
127805
  const response = yield* request(`${REGISTRY_SEARCH_URL}?q=${encodeURIComponent(query)}`).pipe(Effect.orElseSucceed(() => null));
@@ -130108,6 +130947,7 @@ const RPC_REQUIRED_SCOPE = /* @__PURE__ */ new Map([
130108
130947
  [WS_METHODS.storageCleanupSweep, AuthOrchestrationOperateScope],
130109
130948
  [WS_METHODS.worktreeCleanupPreview, AuthOrchestrationReadScope],
130110
130949
  [WS_METHODS.worktreeCleanupSweep, AuthOrchestrationOperateScope],
130950
+ [WS_METHODS.hubAnalyticsQuery, AuthOrchestrationReadScope],
130111
130951
  [WS_METHODS.hubGetSyncStatus, AuthOrchestrationReadScope],
130112
130952
  [WS_METHODS.hubConnect, AuthOrchestrationOperateScope],
130113
130953
  [WS_METHODS.hubDisconnect, AuthOrchestrationOperateScope],
@@ -131135,6 +131975,13 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker, computerUseBrok
131135
131975
  [WS_METHODS.storageCleanupSweep]: (_input) => observeRpcEffect$1(WS_METHODS.storageCleanupSweep, storageCleanup.sweep, { "rpc.aggregate": "storageCleanup" }),
131136
131976
  [WS_METHODS.worktreeCleanupPreview]: (_input) => observeRpcEffect$1(WS_METHODS.worktreeCleanupPreview, worktreeCleanup.preview, { "rpc.aggregate": "worktreeCleanup" }),
131137
131977
  [WS_METHODS.worktreeCleanupSweep]: (_input) => observeRpcEffect$1(WS_METHODS.worktreeCleanupSweep, worktreeCleanup.sweep, { "rpc.aggregate": "worktreeCleanup" }),
131978
+ [WS_METHODS.hubAnalyticsQuery]: (query) => observeRpcEffect$1(WS_METHODS.hubAnalyticsQuery, Effect.gen(function* () {
131979
+ const state = yield* hubLink.current;
131980
+ if (Option.isNone(state.settings)) return yield* new HubLinkError({ detail: "No hub is configured." });
131981
+ const transport = yield* makeHubActivityTransport(state.settings.value);
131982
+ const membership = yield* transport.identity();
131983
+ return yield* transport.queryFacts(query, membership);
131984
+ }).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause instanceof HubLinkError ? cause.detail : "The hub could not be reached." }))), { "rpc.aggregate": "hub" }),
131138
131985
  [WS_METHODS.hubGetSyncStatus]: (_input) => observeRpcEffect$1(WS_METHODS.hubGetSyncStatus, assetSync.status, { "rpc.aggregate": "hub" }),
131139
131986
  [WS_METHODS.hubConnect]: (input) => observeRpcEffect$1(WS_METHODS.hubConnect, hubLink.connect(input).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(assetSync.status)), { "rpc.aggregate": "hub" }),
131140
131987
  [WS_METHODS.hubDisconnect]: (_input) => observeRpcEffect$1(WS_METHODS.hubDisconnect, hubLink.disconnect.pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(assetSync.status)), { "rpc.aggregate": "hub" }),
@@ -132332,6 +133179,23 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
132332
133179
  ...event,
132333
133180
  requestId: RuntimeRequestId.make(String(pending.requestId))
132334
133181
  };
133182
+ /**
133183
+ * Stamp a compacted event with how long its pass actually took.
133184
+ *
133185
+ * The service owns both ends of a compaction, so it is the only place that
133186
+ * can measure one; every reader downstream would be guessing from
133187
+ * surrounding timestamps instead.
133188
+ */
133189
+ const withCompactionMeasurement = (event, pending, nowMs) => event.type === "thread.state.changed" && event.payload.state === "compacted" ? {
133190
+ ...event,
133191
+ payload: {
133192
+ ...event.payload,
133193
+ detail: {
133194
+ ...typeof event.payload.detail === "object" && event.payload.detail !== null ? event.payload.detail : {},
133195
+ durationMs: Math.max(0, nowMs - pending.startedAtMs)
133196
+ }
133197
+ }
133198
+ } : event;
132335
133199
  const compactionTerminal = (event) => event.type === "turn.completed" ? event.payload.state : event.type === "runtime.error" || event.type === "turn.aborted" || event.type === "session.exited" ? event.type : null;
132336
133200
  const processFallbackCompactionEvent = (pending, event) => Effect.gen(function* () {
132337
133201
  if (pendingCompactions.get(event.threadId) !== pending) {
@@ -132341,7 +133205,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
132341
133205
  const matchesTurn = event.turnId !== void 0 && event.turnId === pending.expectedTurnId;
132342
133206
  if (matchesTurn && isCompactedEvent(event)) {
132343
133207
  pending.compactedEventObserved = true;
132344
- yield* publishRuntimeEvent(withCompactionRequestId(event, pending));
133208
+ yield* publishRuntimeEvent(withCompactionMeasurement(withCompactionRequestId(event, pending), pending, yield* Clock.currentTimeMillis));
132345
133209
  return;
132346
133210
  }
132347
133211
  yield* publishRuntimeEvent(event);
@@ -132354,7 +133218,10 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
132354
133218
  type: "thread.state.changed",
132355
133219
  payload: {
132356
133220
  state: "compacted",
132357
- detail: { source: "provider-native-command" }
133221
+ detail: {
133222
+ source: "provider-native-command",
133223
+ durationMs: Math.max(0, (yield* Clock.currentTimeMillis) - pending.startedAtMs)
133224
+ }
132358
133225
  },
132359
133226
  ...pending.requestId !== void 0 ? { requestId: RuntimeRequestId.make(String(pending.requestId)) } : {}
132360
133227
  };
@@ -132386,7 +133253,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
132386
133253
  if (pendingCompaction.native) {
132387
133254
  const compacted = isCompactedEvent(canonicalEvent);
132388
133255
  const terminal = compacted ? "completed" : canonicalEvent.type === "turn.completed" && canonicalEvent.payload.state === "completed" ? null : compactionTerminal(canonicalEvent);
132389
- yield* publishRuntimeEvent(compacted ? withCompactionRequestId(canonicalEvent, pendingCompaction) : canonicalEvent);
133256
+ yield* publishRuntimeEvent(compacted ? withCompactionMeasurement(withCompactionRequestId(canonicalEvent, pendingCompaction), pendingCompaction, yield* Clock.currentTimeMillis) : canonicalEvent);
132390
133257
  if (terminal !== null) yield* settleCompaction(canonicalEvent.threadId, pendingCompaction, terminal);
132391
133258
  return;
132392
133259
  }
@@ -132685,6 +133552,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
132685
133552
  const completion = yield* Deferred.make();
132686
133553
  const pending = {
132687
133554
  completion,
133555
+ startedAtMs: yield* Clock.currentTimeMillis,
132688
133556
  native: compaction.type === "native",
132689
133557
  providerInstanceId: routed.instanceId,
132690
133558
  requestId,
@@ -133660,22 +134528,27 @@ PreviewToolkit.toLayer(handlers$5);
133660
134528
  //#endregion
133661
134529
  //#region src/mcp/TicketResolver.ts
133662
134530
  var TicketResolver = class extends Context.Service()("@p4code/cli/mcp/TicketResolver") {};
134531
+ /**
134532
+ * The client's reasons map one-to-one except `failed`, which for a caller
134533
+ * here means "try again", not "fix your configuration".
134534
+ */
134535
+ const toResolveError = (cause) => new TicketResolveError({
134536
+ reason: cause.reason === "failed" ? "unavailable" : cause.reason,
134537
+ detail: cause.detail
134538
+ });
133663
134539
  const stringField = (record, key) => {
133664
134540
  const value = record[key];
133665
134541
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
133666
134542
  };
133667
134543
  const make$12 = Effect.gen(function* () {
133668
134544
  const linear = yield* LinearClient;
133669
- return { resolve: Effect.fn("TicketResolver.resolve")(function* (reference) {
134545
+ const resolve = Effect.fn("TicketResolver.resolve")(function* (reference) {
133670
134546
  const identifier = parseTicketReference(reference);
133671
134547
  if (identifier === null) return yield* new TicketResolveError({
133672
134548
  reason: "not_found",
133673
134549
  detail: `"${reference}" does not look like a ticket reference.`
133674
134550
  });
133675
- const issue = readOne(yield* linear.call(LINEAR_GET_ISSUE_TOOL, { id: identifier }).pipe(Effect.mapError((cause) => new TicketResolveError({
133676
- reason: cause.reason === "failed" ? "unavailable" : cause.reason,
133677
- detail: cause.detail
133678
- }))));
134551
+ const issue = readOne(yield* linear.call(LINEAR_GET_ISSUE_TOOL, { id: identifier }).pipe(Effect.mapError(toResolveError)));
133679
134552
  const url = issue === void 0 ? void 0 : stringField(issue, "url");
133680
134553
  const title = issue === void 0 ? void 0 : stringField(issue, "title");
133681
134554
  if (issue === void 0 || url === void 0 || title === void 0) return yield* new TicketResolveError({
@@ -133693,7 +134566,43 @@ const make$12 = Effect.gen(function* () {
133693
134566
  title,
133694
134567
  ...description === void 0 ? {} : { description }
133695
134568
  };
133696
- }) };
134569
+ });
134570
+ return {
134571
+ resolve,
134572
+ comment: Effect.fn("TicketResolver.comment")(function* (input) {
134573
+ const ticket = yield* resolve(input.reference);
134574
+ const identifier = ticket.externalRef.identifier;
134575
+ const comment = readOne(yield* linear.call(LINEAR_SAVE_COMMENT_TOOL, {
134576
+ issueId: identifier,
134577
+ body: input.body
134578
+ }).pipe(Effect.mapError(toResolveError)));
134579
+ const commentUrl = comment === void 0 ? void 0 : stringField(comment, "url");
134580
+ const attachedUrls = [];
134581
+ const failedAttachments = [];
134582
+ for (const attachment of input.attachments ?? []) {
134583
+ const failure = yield* linear.call(LINEAR_CREATE_ATTACHMENT_TOOL, {
134584
+ issueId: identifier,
134585
+ url: attachment.url,
134586
+ ...attachment.title === void 0 ? {} : { title: attachment.title }
134587
+ }).pipe(Effect.as(void 0), Effect.catch((cause) => Effect.logWarning("ticket attachment failed", {
134588
+ ticket: identifier,
134589
+ url: attachment.url,
134590
+ detail: cause.detail
134591
+ }).pipe(Effect.as(cause.detail))));
134592
+ if (failure === void 0) attachedUrls.push(attachment.url);
134593
+ else failedAttachments.push({
134594
+ url: attachment.url,
134595
+ detail: failure
134596
+ });
134597
+ }
134598
+ return {
134599
+ externalRef: ticket.externalRef,
134600
+ ...commentUrl === void 0 ? {} : { commentUrl },
134601
+ attachedUrls,
134602
+ failedAttachments
134603
+ };
134604
+ })
134605
+ };
133697
134606
  });
133698
134607
  const layer$5 = Layer.effect(TicketResolver, make$12);
133699
134608
  //#endregion
@@ -133814,7 +134723,29 @@ const TicketResolveTool = Tool.make("ticket_resolve", {
133814
134723
  failure: Schema$1.Union([TaskToolUnavailableError, TicketResolveError]),
133815
134724
  dependencies: [McpInvocationContext, TicketResolver]
133816
134725
  }).annotate(Tool.Title, "Resolve a ticket").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
133817
- const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, TaskCreateTool, TaskUpdateTool, TaskProposeTool, TicketResolveTool);
134726
+ /**
134727
+ * Writing one comment back to the ticket, through p4code's own transport.
134728
+ *
134729
+ * Separate from `ticket_resolve` because it writes, and on the task toolkit
134730
+ * for the same reason that one is: it is the tracker side of the work a board
134731
+ * row describes, under the one capability a session is granted.
134732
+ *
134733
+ * It exists because a provider's own tracker registration is not a transport
134734
+ * p4code controls - it can be absent, or hold a credential the tracker has
134735
+ * stopped accepting - while closeout is the moment that must not fail
134736
+ * silently. `LinearClient` picks the stored API key when there is one and the
134737
+ * registered MCP server otherwise, so this reaches the ticket whenever any
134738
+ * route to it exists. Both halves of a Fusion pair may call it; which half
134739
+ * should is the prompt's business, not the toolkit's.
134740
+ */
134741
+ const TicketCommentTool = Tool.make("ticket_comment", {
134742
+ description: "Comment on a tracker ticket by id or URL (e.g. MOBILE-12241, or a linear.app link), optionally filing links as attachments alongside it. Use it for an authorized closeout note or a requested ticket comment - never for progress chatter. Reaches the tracker through p4code's own credential, so it works when the session has no tracker server of its own. The comment is written as given: Markdown, and the caller's words. Attachments take publicly reachable URLs; a local file path cannot be filed. A link the tracker refuses comes back in failedAttachments with the reason - retry that link alone rather than the whole call, which would file the comment twice.",
134743
+ parameters: TicketCommentInput,
134744
+ success: TicketCommentResult,
134745
+ failure: Schema$1.Union([TaskToolUnavailableError, TicketResolveError]),
134746
+ dependencies: [McpInvocationContext, TicketResolver]
134747
+ }).annotate(Tool.Title, "Comment on a ticket").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
134748
+ const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, TaskCreateTool, TaskUpdateTool, TaskProposeTool, TicketResolveTool, TicketCommentTool);
133818
134749
  //#endregion
133819
134750
  //#region src/mcp/toolkits/tasks/handlers.ts
133820
134751
  const nowIso$1 = Effect.map(DateTime.now, DateTime.formatIso);
@@ -133917,6 +134848,10 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
133917
134848
  yield* requireTaskCapability();
133918
134849
  return yield* (yield* TicketResolver).resolve(input.reference);
133919
134850
  }),
134851
+ ticket_comment: (input) => Effect.gen(function* () {
134852
+ yield* requireTaskCapability();
134853
+ return yield* (yield* TicketResolver).comment(input);
134854
+ }),
133920
134855
  task_propose: () => Effect.gen(function* () {
133921
134856
  yield* requireTaskCapability();
133922
134857
  return { awaitingConfirmation: true };
@@ -136107,7 +137042,7 @@ const watchesAsFusionSupervisor = Effect.fn("mcp.watch.watchesAsFusionSupervisor
136107
137042
  return (yield* (yield* ProjectionSnapshotQuery).getActiveThreadPairs().pipe(Effect.orElseSucceed(() => []))).some((pair) => pair.watcherThreadId === readerThreadId && pair.implementerThreadId === watchedThreadId);
136108
137043
  });
136109
137044
  /** Bounded so one report cannot dominate a thread's activity storage. */
136110
- const clip = (value, limit) => {
137045
+ const clip$1 = (value, limit) => {
136111
137046
  const trimmed = value?.trim();
136112
137047
  if (!trimmed) return null;
136113
137048
  return trimmed.length <= limit ? trimmed : `${trimmed.slice(0, limit)}…`;
@@ -136143,8 +137078,8 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
136143
137078
  serverReadActivityId: serverEvidenceReadActivityId(input.threadId, input.throughSequence),
136144
137079
  closedAt: createdAt
136145
137080
  });
136146
- const summary = clip(input.summary, THREAD_EVIDENCE_REPORT_MAX_CHARS);
136147
- const reason = clip(input.reason, 500);
137081
+ const summary = clip$1(input.summary, THREAD_EVIDENCE_REPORT_MAX_CHARS);
137082
+ const reason = clip$1(input.reason, 500);
136148
137083
  const outcome = summary === null && input.outcome === "summarized" ? "failed" : input.outcome;
136149
137084
  return {
136150
137085
  recorded: yield* engine.dispatch({
@@ -136166,7 +137101,7 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
136166
137101
  throughSequence: input.throughSequence,
136167
137102
  outcome,
136168
137103
  summary,
136169
- model: clip(input.model, MODEL_ID_MAX_CHARS),
137104
+ model: clip$1(input.model, MODEL_ID_MAX_CHARS),
136170
137105
  reason: reason ?? (outcome === "summarized" ? null : "the supervisor gave no reason"),
136171
137106
  context,
136172
137107
  timing
@@ -136354,118 +137289,6 @@ const ComputerUseMcpLive = ComputerUseToolkitRegistrationLive.pipe(Layer.provide
136354
137289
  }).pipe(Layer.provide(McpAuthMiddlewareLive))));
136355
137290
  const layer$4 = Layer.mergeAll(Layer.fresh(StandardMcpLive), Layer.fresh(ComputerUseMcpLive));
136356
137291
  //#endregion
136357
- //#region src/hub/HubActivityTransport.ts
136358
- const REQUEST_TIMEOUT_MS = 8e3;
136359
- const CLEANUP_TIMEOUT_MS = 1e3;
136360
- const MAX_RESPONSE_BYTES = 4096;
136361
- const MAX_REQUEST_BYTES = 16 * 1024;
136362
- const MAX_BEARER_CHARS = 8192;
136363
- var HubActivityUnavailable = class extends Data.TaggedError("HubActivityUnavailable") {};
136364
- var HubActivityUnauthorized = class extends Data.TaggedError("HubActivityUnauthorized") {};
136365
- /** All errors are payload-free; late responses still cancel their bodies after owner interruption. */
136366
- const makeHubActivityTransport = Effect.fn("HubActivityTransport.make")(function* (settings, fetcher = fetch) {
136367
- const origin = yield* Effect.try({
136368
- try: () => {
136369
- const url = new URL(settings.baseUrl);
136370
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/" || settings.token.length === 0 || settings.token.length > MAX_BEARER_CHARS || !/^[\x21-\x7e]+$/.test(settings.token)) throw new HubActivityUnavailable();
136371
- return url.origin;
136372
- },
136373
- catch: () => new HubActivityUnavailable()
136374
- });
136375
- const request = Effect.fn("HubActivityTransport.request")(function* (path, body, membership) {
136376
- return yield* Effect.tryPromise({
136377
- try: async (signal) => {
136378
- const controller = new AbortController();
136379
- const abort = () => controller.abort();
136380
- signal.addEventListener("abort", abort, { once: true });
136381
- if (signal.aborted) abort();
136382
- const timer = setTimeout(abort, REQUEST_TIMEOUT_MS);
136383
- let reader;
136384
- const cancel = async (work) => {
136385
- let timeout;
136386
- try {
136387
- await Promise.race([Promise.resolve().then(work).catch(() => void 0), new Promise((resolve) => {
136388
- timeout = setTimeout(resolve, CLEANUP_TIMEOUT_MS);
136389
- })]);
136390
- } finally {
136391
- if (timeout) clearTimeout(timeout);
136392
- }
136393
- };
136394
- try {
136395
- if (controller.signal.aborted) throw new HubActivityUnavailable();
136396
- const response = await fetcher(`${origin}${path}`, {
136397
- method: body === void 0 ? "GET" : "POST",
136398
- headers: {
136399
- authorization: `Bearer ${settings.token}`,
136400
- ...membership ? {
136401
- "x-p4-hub-account": membership.accountId,
136402
- "x-p4-hub-environment": membership.environmentId
136403
- } : {},
136404
- ...body === void 0 ? {} : { "content-type": "application/json" }
136405
- },
136406
- body,
136407
- signal: controller.signal,
136408
- redirect: "error",
136409
- credentials: "omit",
136410
- cache: "no-store"
136411
- });
136412
- if (controller.signal.aborted) {
136413
- await cancel(() => response.body?.cancel() ?? Promise.resolve());
136414
- throw new HubActivityUnavailable();
136415
- }
136416
- reader = response.body?.getReader();
136417
- if (response.status === 401 || response.status === 403) throw new HubActivityUnauthorized();
136418
- if (response.status !== 200 || !/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") ?? "") || !reader) throw new HubActivityUnavailable();
136419
- let length = 0;
136420
- const chunks = [];
136421
- while (true) {
136422
- const item = await reader.read();
136423
- if (controller.signal.aborted) throw new HubActivityUnavailable();
136424
- if (item.done) break;
136425
- length += item.value.byteLength;
136426
- if (length > MAX_RESPONSE_BYTES) throw new HubActivityUnavailable();
136427
- if (item.value.byteLength) chunks.push(item.value);
136428
- }
136429
- const bytes = new Uint8Array(length);
136430
- let offset = 0;
136431
- for (const chunk of chunks) {
136432
- bytes.set(chunk, offset);
136433
- offset += chunk.byteLength;
136434
- }
136435
- return Schema$1.decodeUnknownSync(Schema$1.UnknownFromJsonString)(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
136436
- } finally {
136437
- controller.abort();
136438
- clearTimeout(timer);
136439
- signal.removeEventListener("abort", abort);
136440
- if (reader) await cancel(() => reader.cancel());
136441
- }
136442
- },
136443
- catch: (error) => error instanceof HubActivityUnauthorized ? error : new HubActivityUnavailable()
136444
- }).pipe(Effect.timeout(9e3), Effect.mapError((error) => error instanceof HubActivityUnauthorized ? error : new HubActivityUnavailable()));
136445
- });
136446
- return {
136447
- identity: Effect.fn("HubActivityTransport.identity")(function* () {
136448
- const membership = yield* request("/v1/identity").pipe(Effect.flatMap(Schema$1.decodeUnknownEffect(HubServerMembership, { onExcessProperty: "error" })), Effect.mapError((error) => error instanceof HubActivityUnauthorized ? error : new HubActivityUnavailable()));
136449
- const destination = yield* Schema$1.encodeUnknownEffect(Schema$1.UnknownFromJsonString)([
136450
- origin,
136451
- membership.accountId,
136452
- membership.environmentId
136453
- ]);
136454
- return {
136455
- ...membership,
136456
- destination: NodeCrypto.createHash("sha256").update(destination).digest("hex")
136457
- };
136458
- }),
136459
- post: Effect.fn("HubActivityTransport.post")(function* (input, membership) {
136460
- const activity = yield* Schema$1.decodeUnknownEffect(HubAgentActivityInput)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => new HubActivityUnavailable()));
136461
- const body = yield* Schema$1.encodeEffect(Schema$1.fromJsonString(HubAgentActivityInput))(activity);
136462
- if (Buffer.byteLength(body) > MAX_REQUEST_BYTES) return yield* Effect.fail(new HubActivityUnavailable());
136463
- const result = yield* request("/v1/agent-activity", body, membership);
136464
- return yield* Schema$1.decodeUnknownEffect(Schema$1.Struct({ accepted: Schema$1.Boolean }))(result, { onExcessProperty: "error" }).pipe(Effect.mapError(() => new HubActivityUnavailable()));
136465
- })
136466
- };
136467
- });
136468
- //#endregion
136469
137292
  //#region src/hub/HubActivityProjection.ts
136470
137293
  const titles = {
136471
137294
  running: "Agent working",
@@ -136596,6 +137419,407 @@ const makeHubActivityOutbound = Effect.fn("HubActivityOutbound.make")(function*
136596
137419
  })
136597
137420
  };
136598
137421
  });
137422
+ const encodeFact = Schema$1.encodeEffect(Schema$1.fromJsonString(HubAnalyticsFactInput));
137423
+ const decodeStoredFact = Schema$1.decodeUnknownEffect(Schema$1.fromJsonString(HubAnalyticsFactInput));
137424
+ /** Stage one derived fact. Returns false when this id was already staged. */
137425
+ const stageAnalyticsFact = Effect.fn("AnalyticsFactOutbox.stage")(function* (fact) {
137426
+ const sql = yield* SqlClient.SqlClient;
137427
+ const payload = yield* encodeFact(fact);
137428
+ const now = yield* Clock.currentTimeMillis;
137429
+ return (yield* sql`
137430
+ INSERT INTO analytics_fact_outbox (event_id, kind, version, occurred_at, payload, pending, created_at)
137431
+ VALUES (${fact.eventId}, ${fact.kind}, ${fact.version}, ${fact.occurredAt}, ${payload}, 1, ${now})
137432
+ ON CONFLICT(event_id) DO NOTHING
137433
+ RETURNING event_id`).length === 1;
137434
+ });
137435
+ /** The oldest undelivered facts, in the order they were captured. */
137436
+ const pendingAnalyticsFacts = Effect.fn("AnalyticsFactOutbox.pending")(function* (limit = 16) {
137437
+ return yield* (yield* SqlClient.SqlClient)`
137438
+ SELECT event_id, payload FROM analytics_fact_outbox WHERE pending = 1
137439
+ ORDER BY created_at, event_id LIMIT ${limit}`;
137440
+ });
137441
+ /**
137442
+ * Mark one fact delivered.
137443
+ *
137444
+ * The hub answers `accepted: false` for a fact it already holds, which is the
137445
+ * same outcome as a fresh accept from here: the row is delivered either way,
137446
+ * and a retry that raced an earlier success must not stay pending forever.
137447
+ */
137448
+ const acknowledgeAnalyticsFact = Effect.fn("AnalyticsFactOutbox.acknowledge")(function* (eventId) {
137449
+ return (yield* (yield* SqlClient.SqlClient)`UPDATE analytics_fact_outbox SET pending = 0
137450
+ WHERE event_id = ${eventId} AND pending = 1 RETURNING event_id`).length === 1;
137451
+ });
137452
+ /** Where a source's scan left off, so a restart resumes instead of replaying. */
137453
+ const readAnalyticsCursor = Effect.fn("AnalyticsFactOutbox.readCursor")(function* (source) {
137454
+ return (yield* (yield* SqlClient.SqlClient)`SELECT after_sequence FROM analytics_fact_cursors WHERE source = ${source}`)[0]?.after_sequence ?? 0;
137455
+ });
137456
+ const writeAnalyticsCursor = Effect.fn("AnalyticsFactOutbox.writeCursor")(function* (source, afterSequence) {
137457
+ yield* (yield* SqlClient.SqlClient)`INSERT INTO analytics_fact_cursors (source, after_sequence)
137458
+ VALUES (${source}, ${afterSequence})
137459
+ ON CONFLICT(source) DO UPDATE SET after_sequence = excluded.after_sequence`;
137460
+ });
137461
+ //#endregion
137462
+ //#region src/analytics/analyticsFacts.ts
137463
+ const MAX_QUESTION_CHARS = 2e3;
137464
+ const MAX_HEADER_CHARS = 120;
137465
+ const MAX_OPTION_LABEL_CHARS = 200;
137466
+ const MAX_OPTION_DESCRIPTION_CHARS = 600;
137467
+ const MAX_PRESENTED_OPTIONS = 12;
137468
+ const MAX_CUSTOM_ANSWER_CHARS = 2e3;
137469
+ const MAX_REASON_CHARS = 200;
137470
+ const MAX_MODEL_CHARS = 120;
137471
+ const clip = (value, max) => value.length <= max ? value : value.slice(0, max);
137472
+ const asRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
137473
+ const asFiniteInt$1 = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(value) : null;
137474
+ /** `question:<requestId>:<questionId>`, so a replayed resolution derives the same id. */
137475
+ const questionFactId = (requestId, questionId) => `question:${requestId}:${questionId}`;
137476
+ /**
137477
+ * One fact per question in a resolved request.
137478
+ *
137479
+ * The presented options travel with the answer because the answer alone says
137480
+ * nothing: "Patch" only means something beside what else was offered, and a
137481
+ * custom answer only means something beside what it refused.
137482
+ */
137483
+ function questionFactsFrom(input) {
137484
+ const questions = Array.isArray(input.questions) ? input.questions : [];
137485
+ const answers = asRecord$3(input.answers) ?? {};
137486
+ const facts = [];
137487
+ for (const raw of questions) {
137488
+ const question = asRecord$3(raw);
137489
+ if (question === null) continue;
137490
+ const questionId = typeof question.id === "string" ? question.id : null;
137491
+ const prompt = typeof question.question === "string" ? question.question : null;
137492
+ if (questionId === null || prompt === null || prompt.length === 0) continue;
137493
+ const options = (Array.isArray(question.options) ? question.options : []).slice(0, MAX_PRESENTED_OPTIONS).flatMap((entry) => {
137494
+ const option = asRecord$3(entry);
137495
+ const label = typeof option?.label === "string" ? option.label : null;
137496
+ return label === null || label.length === 0 ? [] : [{
137497
+ label: clip(label, MAX_OPTION_LABEL_CHARS),
137498
+ description: clip(typeof option?.description === "string" ? option.description : "", MAX_OPTION_DESCRIPTION_CHARS)
137499
+ }];
137500
+ });
137501
+ const given = answers[questionId];
137502
+ const selected = (Array.isArray(given) ? given : given === void 0 ? [] : [given]).flatMap((value) => typeof value === "string" && value.length > 0 ? [value] : []);
137503
+ const indexes = selected.map((label) => options.findIndex((option) => option.label === label));
137504
+ const custom = selected.find((_, position) => indexes[position] === -1);
137505
+ const answer = selected.length === 0 ? { kind: "dismissed" } : custom !== void 0 ? {
137506
+ kind: "custom",
137507
+ text: clip(custom, MAX_CUSTOM_ANSWER_CHARS)
137508
+ } : {
137509
+ kind: "option",
137510
+ optionIndexes: indexes,
137511
+ labels: selected.map((label) => clip(label, MAX_OPTION_LABEL_CHARS))
137512
+ };
137513
+ facts.push({
137514
+ eventId: questionFactId(input.requestId, questionId),
137515
+ occurredAt: input.occurredAt,
137516
+ kind: "question.answered",
137517
+ version: 1,
137518
+ payload: {
137519
+ questionId,
137520
+ threadId: input.threadId,
137521
+ header: clip(typeof question.header === "string" ? question.header : "", MAX_HEADER_CHARS),
137522
+ question: clip(prompt, MAX_QUESTION_CHARS),
137523
+ multiSelect: question.multiSelect === true,
137524
+ options,
137525
+ answer
137526
+ }
137527
+ });
137528
+ }
137529
+ return facts;
137530
+ }
137531
+ /** `fusion:<threadId>:<throughSequence>`: one handoff covers one range, once. */
137532
+ const fusionFactId = (threadId, throughSequence) => `fusion:${threadId}:${throughSequence}`;
137533
+ /**
137534
+ * One review, read from the two rows that recorded it.
137535
+ *
137536
+ * The outcome, the model and the duration come from the report, because that
137537
+ * is the row the server wrote them on; the coverage and the summary come from
137538
+ * the handoff, because that is the row that carried them. Neither is guessed
137539
+ * from the other, and a handoff with no report yields nothing at all.
137540
+ */
137541
+ function fusionHandoffFactFrom(input) {
137542
+ const payload = asRecord$3(input.payload);
137543
+ const report = asRecord$3(input.reportPayload);
137544
+ if (payload === null || report === null) return null;
137545
+ const outcome = report.outcome;
137546
+ if (outcome !== "summarized" && outcome !== "fallback" && outcome !== "failed" && outcome !== "unreported") return null;
137547
+ const coverage = asRecord$3(payload.coverage) ?? {};
137548
+ const context = asRecord$3(report.context);
137549
+ const timing = asRecord$3(report.timing);
137550
+ const summary = typeof payload.summary === "string" ? payload.summary : null;
137551
+ const reason = typeof report.reason === "string" ? clip(report.reason, MAX_REASON_CHARS) : null;
137552
+ return {
137553
+ eventId: fusionFactId(input.threadId, input.throughSequence),
137554
+ occurredAt: input.occurredAt,
137555
+ kind: "fusion.handoff",
137556
+ version: 1,
137557
+ payload: {
137558
+ fusionVersion: input.fusionVersion,
137559
+ threadId: input.threadId,
137560
+ status: outcome,
137561
+ model: typeof report.model === "string" ? clip(report.model, MAX_MODEL_CHARS) : null,
137562
+ reason,
137563
+ transcriptEvents: asFiniteInt$1(coverage.transcriptEvents) ?? 0,
137564
+ transcriptIncluded: asFiniteInt$1(coverage.transcriptIncluded) ?? 0,
137565
+ transcriptDropped: asFiniteInt$1(coverage.transcriptDropped) ?? 0,
137566
+ transcriptClippedFields: asFiniteInt$1(coverage.transcriptClippedFields) ?? 0,
137567
+ ledgerRows: asFiniteInt$1(coverage.ledgerRows) ?? 0,
137568
+ ledgerTotalRows: asFiniteInt$1(coverage.ledgerTotalRows) ?? 0,
137569
+ summaryChars: summary === null ? null : summary.length,
137570
+ contextDeltaTokens: asFiniteInt$1(context?.deltaTokens),
137571
+ durationMs: asFiniteInt$1(timing?.durationMs)
137572
+ }
137573
+ };
137574
+ }
137575
+ /** `compression:<threadId>:<turnId>`: one pass per turn, once. */
137576
+ const compressionFactId = (threadId, turnId) => `compression:${threadId}:${turnId}`;
137577
+ /**
137578
+ * A compression pass, only when it was actually measured.
137579
+ *
137580
+ * Both token readings and the duration have to come from recorded state: a
137581
+ * pass whose before or after was never written is not reported as zero, and
137582
+ * the delta is derived from the two readings rather than supplied beside them.
137583
+ */
137584
+ function compressionFactFrom(input) {
137585
+ const before = asFiniteInt$1(input.beforeTokens);
137586
+ const after = asFiniteInt$1(input.afterTokens);
137587
+ const durationMs = asFiniteInt$1(input.durationMs);
137588
+ if (before === null || after === null || durationMs === null || durationMs < 0) return null;
137589
+ if (before < 0 || after < 0) return null;
137590
+ return {
137591
+ eventId: compressionFactId(input.threadId, input.turnId),
137592
+ occurredAt: input.occurredAt,
137593
+ kind: "compression.applied",
137594
+ version: 1,
137595
+ payload: {
137596
+ mode: input.mode,
137597
+ threadId: input.threadId,
137598
+ beforeTokens: before,
137599
+ afterTokens: after,
137600
+ deltaTokens: before - after,
137601
+ durationMs
137602
+ }
137603
+ };
137604
+ }
137605
+ //#endregion
137606
+ //#region src/analytics/AnalyticsFactCapture.ts
137607
+ const ANALYTICS_QUESTION_SOURCE = "events.user-input";
137608
+ const ANALYTICS_FUSION_SOURCE = "events.fusion-evidence";
137609
+ const ANALYTICS_COMPRESSION_SOURCE = "events.context-compaction";
137610
+ const decodeJson$1 = Schema$1.decodeUnknownEffect(Schema$1.fromJsonString(Schema$1.Unknown));
137611
+ const asRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
137612
+ const asFiniteInt = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(value) : null;
137613
+ const asUsedTokens = (payload) => asFiniteInt(asRecord$2(payload)?.usedTokens);
137614
+ /** One appended activity, read out of its event envelope. */
137615
+ const readActivityEvent = Effect.fn("AnalyticsFactCapture.activity")(function* (row) {
137616
+ const payload = asRecord$2(yield* decodeJson$1(row.payload_json));
137617
+ const activity = asRecord$2(payload?.activity);
137618
+ const threadId = typeof payload?.threadId === "string" ? payload.threadId : null;
137619
+ if (activity === null || threadId === null) return null;
137620
+ return {
137621
+ sequence: row.sequence,
137622
+ threadId,
137623
+ turnId: typeof activity.turnId === "string" ? activity.turnId : null,
137624
+ createdAt: typeof activity.createdAt === "string" ? activity.createdAt : "",
137625
+ payload: asRecord$2(activity.payload) ?? {}
137626
+ };
137627
+ });
137628
+ /** Candidate rows of one activity kind, after a source's own cursor. */
137629
+ const readCandidates = Effect.fn("AnalyticsFactCapture.candidates")(function* (kind, after, limit) {
137630
+ return yield* (yield* SqlClient.SqlClient)`
137631
+ SELECT sequence, payload_json FROM orchestration_events
137632
+ WHERE sequence > ${after} AND event_type = 'thread.activity-appended'
137633
+ AND json_extract(payload_json, '$.activity.kind') = ${kind}
137634
+ ORDER BY sequence ASC LIMIT ${limit}`;
137635
+ });
137636
+ /**
137637
+ * The request a resolution answers, matched on the exact request id.
137638
+ *
137639
+ * A newer question in the same thread must never lend its options to an older
137640
+ * answer, which is what a "most recent request" lookup would do whenever two
137641
+ * requests were open at once.
137642
+ */
137643
+ const readQuestionRequest = Effect.fn("AnalyticsFactCapture.questionRequest")(function* (threadId, requestId, throughSequence) {
137644
+ const [row] = yield* (yield* SqlClient.SqlClient)`
137645
+ SELECT sequence, payload_json FROM orchestration_events
137646
+ WHERE sequence <= ${throughSequence} AND event_type = 'thread.activity-appended'
137647
+ AND json_extract(payload_json, '$.activity.kind') = 'user-input.requested'
137648
+ AND json_extract(payload_json, '$.threadId') = ${threadId}
137649
+ AND json_extract(payload_json, '$.activity.payload.requestId') = ${requestId}
137650
+ ORDER BY sequence DESC LIMIT 1`;
137651
+ if (row === void 0) return null;
137652
+ return (yield* readActivityEvent(row))?.payload ?? null;
137653
+ });
137654
+ /** The handoff a report closes, found by the range the report names. */
137655
+ const readHandoff = Effect.fn("AnalyticsFactCapture.handoff")(function* (threadId, throughSequence, beforeSequence) {
137656
+ const [row] = yield* (yield* SqlClient.SqlClient)`
137657
+ SELECT sequence, payload_json FROM orchestration_events
137658
+ WHERE sequence <= ${beforeSequence} AND event_type = 'thread.activity-appended'
137659
+ AND json_extract(payload_json, '$.activity.kind') = 'fusion.evidence.handoff'
137660
+ AND json_extract(payload_json, '$.activity.payload.watchedThreadId') = ${threadId}
137661
+ AND json_extract(payload_json, '$.activity.payload.throughSequence') = ${throughSequence}
137662
+ ORDER BY sequence DESC LIMIT 1`;
137663
+ if (row === void 0) return null;
137664
+ return (yield* readActivityEvent(row))?.payload ?? null;
137665
+ });
137666
+ /**
137667
+ * The nearest context reading on one side of a compaction, in the same turn.
137668
+ *
137669
+ * Correlating on the turn is the point: a later turn's context update must
137670
+ * never be able to close an old pass, which is exactly what "nearest reading
137671
+ * in this thread" would let it do once the pass itself was never completed.
137672
+ */
137673
+ const readContextReading = Effect.fn("AnalyticsFactCapture.contextReading")(function* (threadId, turnId, sequence, side) {
137674
+ const sql = yield* SqlClient.SqlClient;
137675
+ const row = (side === "before" ? yield* sql`
137676
+ SELECT sequence, payload_json FROM orchestration_events
137677
+ WHERE sequence < ${sequence} AND event_type = 'thread.activity-appended'
137678
+ AND json_extract(payload_json, '$.activity.kind') = 'context-window.updated'
137679
+ AND json_extract(payload_json, '$.threadId') = ${threadId}
137680
+ AND json_extract(payload_json, '$.activity.turnId') = ${turnId}
137681
+ ORDER BY sequence DESC LIMIT 1` : yield* sql`
137682
+ SELECT sequence, payload_json FROM orchestration_events
137683
+ WHERE sequence > ${sequence} AND event_type = 'thread.activity-appended'
137684
+ AND json_extract(payload_json, '$.activity.kind') = 'context-window.updated'
137685
+ AND json_extract(payload_json, '$.threadId') = ${threadId}
137686
+ AND json_extract(payload_json, '$.activity.turnId') = ${turnId}
137687
+ ORDER BY sequence ASC LIMIT 1`)[0];
137688
+ return row === void 0 ? null : yield* readActivityEvent(row);
137689
+ });
137690
+ const stageAll = Effect.fn("AnalyticsFactCapture.stageAll")(function* (facts) {
137691
+ let staged = 0;
137692
+ for (const fact of facts) if (yield* stageAnalyticsFact(fact)) staged += 1;
137693
+ return staged;
137694
+ });
137695
+ /** Questions: one fact per question in each resolved request. */
137696
+ const captureQuestionFacts = Effect.fn("AnalyticsFactCapture.questions")(function* (limit = 128) {
137697
+ const after = yield* readAnalyticsCursor(ANALYTICS_QUESTION_SOURCE);
137698
+ const rows = yield* readCandidates("user-input.resolved", after, limit);
137699
+ let cursor = after;
137700
+ let staged = 0;
137701
+ for (const row of rows) {
137702
+ const event = yield* readActivityEvent(row);
137703
+ const requestId = typeof event?.payload.requestId === "string" ? event.payload.requestId : null;
137704
+ if (event !== null && requestId !== null) {
137705
+ const request = yield* readQuestionRequest(event.threadId, requestId, event.sequence);
137706
+ if (request !== null) staged += yield* stageAll(questionFactsFrom({
137707
+ requestId,
137708
+ threadId: event.threadId,
137709
+ occurredAt: event.createdAt,
137710
+ questions: request.questions,
137711
+ answers: event.payload.answers
137712
+ }));
137713
+ }
137714
+ cursor = row.sequence;
137715
+ }
137716
+ if (cursor !== after) yield* writeAnalyticsCursor(ANALYTICS_QUESTION_SOURCE, cursor);
137717
+ return {
137718
+ staged,
137719
+ cursor
137720
+ };
137721
+ });
137722
+ /**
137723
+ * Fusion: staged from the evidence report, never before it.
137724
+ *
137725
+ * The report is what carries the model, the context delta and the measured
137726
+ * duration, so a fact staged at handoff time would be missing exactly the
137727
+ * numbers it exists to carry - and could never gain them, because the fact is
137728
+ * append-only.
137729
+ */
137730
+ const captureFusionFacts = Effect.fn("AnalyticsFactCapture.fusion")(function* (input) {
137731
+ const after = yield* readAnalyticsCursor(ANALYTICS_FUSION_SOURCE);
137732
+ const rows = yield* readCandidates("fusion.evidence.summary", after, input.limit ?? 128);
137733
+ let cursor = after;
137734
+ let staged = 0;
137735
+ for (const row of rows) {
137736
+ const event = yield* readActivityEvent(row);
137737
+ const watchedThreadId = typeof event?.payload.watchedThreadId === "string" ? event.payload.watchedThreadId : null;
137738
+ const throughSequence = asFiniteInt(event?.payload.throughSequence);
137739
+ if (event !== null && watchedThreadId !== null && throughSequence !== null) {
137740
+ const handoff = yield* readHandoff(watchedThreadId, throughSequence, event.sequence);
137741
+ if (handoff !== null) {
137742
+ const fact = fusionHandoffFactFrom({
137743
+ threadId: watchedThreadId,
137744
+ throughSequence,
137745
+ occurredAt: event.createdAt,
137746
+ fusionVersion: input.fusionVersion,
137747
+ payload: handoff,
137748
+ reportPayload: event.payload
137749
+ });
137750
+ if (fact !== null) staged += yield* stageAll([fact]);
137751
+ }
137752
+ }
137753
+ cursor = row.sequence;
137754
+ }
137755
+ if (cursor !== after) yield* writeAnalyticsCursor(ANALYTICS_FUSION_SOURCE, cursor);
137756
+ return {
137757
+ staged,
137758
+ cursor
137759
+ };
137760
+ });
137761
+ /**
137762
+ * Compression: only a pass the service measured and the provider reported.
137763
+ *
137764
+ * The duration comes from the compaction event the provider service stamped,
137765
+ * the tokens from the recorded context readings either side. A pass whose
137766
+ * later reading has not landed yet holds back only this cursor, so a question
137767
+ * or a handoff behind it is still captured on the same tick.
137768
+ */
137769
+ const captureCompressionFacts = Effect.fn("AnalyticsFactCapture.compression")(function* (limit = 128) {
137770
+ const after = yield* readAnalyticsCursor(ANALYTICS_COMPRESSION_SOURCE);
137771
+ const rows = yield* readCandidates("context-compaction", after, limit);
137772
+ let cursor = after;
137773
+ let staged = 0;
137774
+ for (const row of rows) {
137775
+ const event = yield* readActivityEvent(row);
137776
+ if (event === null || event.turnId === null) {
137777
+ cursor = row.sequence;
137778
+ continue;
137779
+ }
137780
+ const durationMs = asFiniteInt(asRecord$2(event.payload.detail)?.durationMs);
137781
+ const before = yield* readContextReading(event.threadId, event.turnId, event.sequence, "before");
137782
+ const afterReading = yield* readContextReading(event.threadId, event.turnId, event.sequence, "after");
137783
+ if (afterReading === null) break;
137784
+ const mode = asRecord$2(before?.payload)?.compressMode ?? afterReading.payload.compressMode;
137785
+ const fact = typeof mode === "string" ? compressionFactFrom({
137786
+ threadId: event.threadId,
137787
+ turnId: event.turnId,
137788
+ occurredAt: event.createdAt,
137789
+ mode,
137790
+ beforeTokens: asUsedTokens(before?.payload),
137791
+ afterTokens: asUsedTokens(afterReading.payload),
137792
+ durationMs
137793
+ }) : null;
137794
+ if (fact !== null) staged += yield* stageAll([fact]);
137795
+ cursor = row.sequence;
137796
+ }
137797
+ if (cursor !== after) yield* writeAnalyticsCursor(ANALYTICS_COMPRESSION_SOURCE, cursor);
137798
+ return {
137799
+ staged,
137800
+ cursor
137801
+ };
137802
+ });
137803
+ /** One tick over every source. A source that stalls never blocks another. */
137804
+ const captureAnalyticsFacts = Effect.fn("AnalyticsFactCapture.run")(function* (input) {
137805
+ const questions = yield* captureQuestionFacts(input.limit);
137806
+ const fusion = yield* captureFusionFacts(input);
137807
+ const compression = yield* captureCompressionFacts(input.limit);
137808
+ return { staged: questions.staged + fusion.staged + compression.staged };
137809
+ });
137810
+ //#endregion
137811
+ //#region src/analytics/AnalyticsDelivery.ts
137812
+ const runAnalyticsTick = Effect.fn("AnalyticsDelivery.tick")(function* (transport, identity, fusionVersion) {
137813
+ yield* captureAnalyticsFacts({ fusionVersion });
137814
+ let delivered = 0;
137815
+ for (const row of yield* pendingAnalyticsFacts()) {
137816
+ const fact = yield* decodeStoredFact(row.payload);
137817
+ yield* transport.postFact(fact, identity);
137818
+ yield* acknowledgeAnalyticsFact(row.event_id);
137819
+ delivered += 1;
137820
+ }
137821
+ return { delivered };
137822
+ });
136599
137823
  //#endregion
136600
137824
  //#region src/hub/HubTunnelReconnect.ts
136601
137825
  const HUB_TUNNEL_RETRY_INITIAL_MS = 1e3;
@@ -136634,6 +137858,7 @@ const runHubActivityTick = Effect.fn("HubActivityRuntime.tick")(function* (outbo
136634
137858
  yield* transport.post(activity, identity);
136635
137859
  yield* outbound.acknowledge(pending.thread_id, pending.revision);
136636
137860
  }
137861
+ yield* runAnalyticsTick(transport, identity, version$1).pipe(Effect.catchCause((cause) => Effect.logDebug("analytics tick deferred", { cause })));
136637
137862
  });
136638
137863
  /** A credential generation owns every request, staging page and delay. Old destinations remain isolated. */
136639
137864
  const runHubActivityEmission = Effect.fn("HubActivityRuntime.run")(function* (settings, ready) {
@@ -141591,22 +142816,95 @@ function describeHandoffEvent(event, implementerThreadId, clip) {
141591
142816
  }
141592
142817
  }
141593
142818
  /**
142819
+ * An activity line's evidence fields, its kind dropped and its title read the
142820
+ * way the ledger reads it: a provider spells the opening fragment's title
142821
+ * "<title> started", which is the same call rather than a different one.
142822
+ */
142823
+ function activityFields(content) {
142824
+ const [, ...fields] = content.split(" | ");
142825
+ const [title, ...rest] = fields;
142826
+ return title === void 0 ? fields : [title.replace(/ started$/u, ""), ...rest];
142827
+ }
142828
+ /** What a line is a fragment of, or null when it stands alone. */
142829
+ function fragmentIdentity(event) {
142830
+ if (event.type === "thread.message-sent") {
142831
+ const messageId = asTrimmed(event.payload.messageId);
142832
+ return messageId === null ? null : `message:${messageId}`;
142833
+ }
142834
+ if (event.type === "thread.activity-appended") {
142835
+ const data = asRecord(asRecord(event.payload.activity.payload)?.data);
142836
+ const toolCallId = asTrimmed(data?.toolCallId);
142837
+ return toolCallId === null ? null : `tool:${toolCallId}`;
142838
+ }
142839
+ return null;
142840
+ }
142841
+ /**
142842
+ * Fragments whose evidence a later line of the same message or call already
142843
+ * carries, so the transcript spends its budget on evidence rather than on the
142844
+ * same text streamed twenty times.
142845
+ *
142846
+ * A streaming message is folded only when a later fragment literally contains
142847
+ * it, and a tool fragment only when every field it carries reappears later, so
142848
+ * nothing unique is ever folded away. What a folded line did carry alone - the
142849
+ * call's identity, its status and the sequence it started at - is exactly what
142850
+ * the ledger records beside this transcript.
142851
+ */
142852
+ function supersededLines(lines) {
142853
+ const groups = /* @__PURE__ */ new Map();
142854
+ for (const line of lines) {
142855
+ if (line.identity === null) continue;
142856
+ const group = groups.get(line.identity);
142857
+ if (group === void 0) groups.set(line.identity, [line]);
142858
+ else group.push(line);
142859
+ }
142860
+ const superseded = /* @__PURE__ */ new Set();
142861
+ for (const group of groups.values()) {
142862
+ if (group.length < 2) continue;
142863
+ const last = group[group.length - 1];
142864
+ for (let index = 0; index < group.length - 1; index += 1) {
142865
+ const line = group[index];
142866
+ if (line.shape === "message") {
142867
+ if (last.shape === "message" && last.content.includes(line.content)) superseded.add(line);
142868
+ continue;
142869
+ }
142870
+ const laterFields = new Set(group.slice(index + 1).flatMap((later) => later.fields));
142871
+ if (line.fields.every((field) => laterFields.has(field))) superseded.add(line);
142872
+ }
142873
+ }
142874
+ return superseded;
142875
+ }
142876
+ /**
141594
142877
  * Flatten the range, newest kept when the bound bites.
141595
142878
  *
141596
- * Dropping the oldest is the lesser evil for the same reason it is in the
141597
- * triage window, but unlike there the loss is reported rather than absorbed:
141598
- * a range that did not fit is a range this handoff must not call complete.
142879
+ * Duplicate fragments are folded before the bound is applied, because dropping
142880
+ * a unique event to make room for the twelfth copy of a streaming message is a
142881
+ * loss this handoff can simply avoid. Dropping the oldest is still the lesser
142882
+ * evil once the bound does bite, and unlike in the triage window the loss is
142883
+ * reported rather than absorbed: a range that did not fit is a range this
142884
+ * handoff must not call complete. Clipped fields are counted on the lines
142885
+ * actually handed over, so a field clipped inside a folded or dropped line is
142886
+ * not charged against coverage twice.
141599
142887
  */
141600
142888
  function buildHandoffEvidence(events, implementerThreadId, maxChars) {
141601
- const clip = makeClipper();
141602
- const lines = [];
142889
+ const rendered = [];
141603
142890
  for (const event of events) {
142891
+ const clip = makeClipper();
141604
142892
  const text = describeHandoffEvent(event, implementerThreadId, clip);
141605
- if (text !== null) lines.push({
142893
+ if (text === null) continue;
142894
+ const content = text.replace(/^#\d+ /u, "");
142895
+ const shape = event.type === "thread.message-sent" ? "message" : "activity";
142896
+ rendered.push({
141606
142897
  sequence: event.sequence,
141607
- text
142898
+ text,
142899
+ clippedFields: clip.clipped,
142900
+ identity: fragmentIdentity(event),
142901
+ shape,
142902
+ content,
142903
+ fields: shape === "activity" ? activityFields(content) : []
141608
142904
  });
141609
142905
  }
142906
+ const superseded = supersededLines(rendered);
142907
+ const lines = rendered.filter((line) => !superseded.has(line));
141610
142908
  const kept = [];
141611
142909
  let used = 0;
141612
142910
  for (let index = lines.length - 1; index >= 0; index -= 1) {
@@ -141618,10 +142916,11 @@ function buildHandoffEvidence(events, implementerThreadId, maxChars) {
141618
142916
  }
141619
142917
  return {
141620
142918
  transcript: kept.map((line) => line.text).join("\n"),
141621
- eventCount: lines.length,
142919
+ eventCount: rendered.length,
141622
142920
  includedCount: kept.length,
141623
142921
  droppedCount: lines.length - kept.length,
141624
- clippedFields: clip.clipped,
142922
+ coalescedCount: superseded.size,
142923
+ clippedFields: kept.reduce((total, line) => total + line.clippedFields, 0),
141625
142924
  firstSequence: kept[0]?.sequence ?? null,
141626
142925
  lastSequence: kept[kept.length - 1]?.sequence ?? null,
141627
142926
  includedSequences: new Set(kept.map((line) => line.sequence))
@@ -141677,6 +142976,29 @@ const HANDOFF_SUMMARY_INSTRUCTIONS = [
141677
142976
  "cite at least one sequence, and never a sequence absent from the transcript",
141678
142977
  "name the user's requirements and corrections, and quote the exact text of any failure or error"
141679
142978
  ];
142979
+ /** Enough sequences to show the shape of a citation without dictating the summary. */
142980
+ const CITATION_EXAMPLE_COUNT = 2;
142981
+ /** The lowest and highest sequence handed over, as the citations they must be copied as. */
142982
+ function citationExamples(includedSequences) {
142983
+ const sorted = [...includedSequences].sort((left, right) => left - right);
142984
+ return (sorted.length <= CITATION_EXAMPLE_COUNT ? sorted : [sorted[0], sorted[sorted.length - 1]]).map((sequence) => `#${sequence}`);
142985
+ }
142986
+ /**
142987
+ * The citation rules carrying real sequences, because `#<sequence>` read as a
142988
+ * placeholder is what a summarizer answers without any number at all.
142989
+ */
142990
+ function handoffSummaryInstructions(input) {
142991
+ const examples = citationExamples(input.includedSequences);
142992
+ return examples.length === 0 ? HANDOFF_SUMMARY_INSTRUCTIONS : [...HANDOFF_SUMMARY_INSTRUCTIONS, `copy citations from the line starts of this transcript, for example ${examples.join(" and ")}`];
142993
+ }
142994
+ /**
142995
+ * The single repair attempt: the same rules, plus what the discarded answer
142996
+ * got wrong. Nothing here relaxes the rule - a second miss falls back.
142997
+ */
142998
+ function handoffSummaryRepairInstructions(input) {
142999
+ const invented = citedSequences(input.summary).filter((sequence) => !input.includedSequences.has(sequence)).slice(0, CITATION_EXAMPLE_COUNT).map((sequence) => `#${sequence}`);
143000
+ return [...handoffSummaryInstructions(input), input.rejection === "summary-uncited" || invented.length === 0 ? "your previous answer was discarded because it cited no sequence from the transcript; every claim needs one" : `your previous answer was discarded because it cited ${invented.join(" and ")}, which this transcript does not contain; never write a number that is not at a line start`];
143001
+ }
141680
143002
  /** How a summarized handoff opens, so a caller can tell one from a fallback. */
141681
143003
  const SERVER_HANDOFF_SUMMARY_PREFIX = "Server evidence handoff for builder thread";
141682
143004
  /**
@@ -141706,6 +143028,7 @@ const EMPTY_EVIDENCE = {
141706
143028
  eventCount: 0,
141707
143029
  includedCount: 0,
141708
143030
  droppedCount: 0,
143031
+ coalescedCount: 0,
141709
143032
  clippedFields: 0,
141710
143033
  firstSequence: null,
141711
143034
  lastSequence: null,
@@ -141765,19 +143088,20 @@ function resolveHandoffModel(settings) {
141765
143088
  };
141766
143089
  }
141767
143090
  /**
141768
- * Whether a summary may stand in for reading the range.
143091
+ * Why a summary may not stand in for reading the range, or null when it may.
141769
143092
  *
141770
- * Empty is refused for the obvious reason. Uncited is refused because a claim
141771
- * with no sequence behind it cannot be checked against the ledger, and a
141772
- * citation outside the window is refused because it is evidence of a model
141773
- * describing something other than this turn.
143093
+ * Empty and uncited are refused because a claim with no sequence behind it
143094
+ * cannot be checked against the ledger, and a citation outside the window is
143095
+ * refused because it is evidence of a model describing something other than
143096
+ * this turn. The two are named apart because they fail differently: one is a
143097
+ * summarizer that ignored the rule, the other one that read something else.
141774
143098
  */
141775
- function summaryIsUsable(input) {
143099
+ function summaryRejection(input) {
141776
143100
  const trimmed = input.summary.trim();
141777
- if (trimmed.length === 0) return false;
143101
+ if (trimmed.length === 0) return "summary-uncited";
141778
143102
  const cited = citedSequences(trimmed);
141779
- if (cited.length === 0) return false;
141780
- return cited.every((sequence) => input.includedSequences.has(sequence));
143103
+ if (cited.length === 0) return "summary-uncited";
143104
+ return cited.every((sequence) => input.includedSequences.has(sequence)) ? null : "summary-citation-invalid";
141781
143105
  }
141782
143106
  const buildFusionEvidenceHandoff = Effect.fn("fusionEvidenceHandoff.build")(function* (input) {
141783
143107
  const engine = yield* OrchestrationEngineService;
@@ -141833,20 +143157,51 @@ const buildFusionEvidenceHandoff = Effect.fn("fusionEvidenceHandoff.build")(func
141833
143157
  if (driver === void 0 || !HARDENED_SUMMARY_DRIVERS.has(driver)) return yield* fallback("provider-profile-unhardened");
141834
143158
  if (textGeneration.summarizeFusionEvidence === void 0) return yield* fallback("provider-cannot-summarize");
141835
143159
  const summarize = textGeneration.summarizeFusionEvidence;
141836
- const produced = yield* Effect.suspend(() => summarize({
143160
+ const runSummary = (instructions) => Effect.suspend(() => summarize({
141837
143161
  cwd: serverConfig.cwd,
141838
143162
  transcript: evidence.transcript,
141839
143163
  maxTranscriptChars: HANDOFF_TRANSCRIPT_MAX_CHARS,
141840
- instructions: HANDOFF_SUMMARY_INSTRUCTIONS,
143164
+ instructions,
141841
143165
  modelSelection: model
141842
- })).pipe(Effect.map((result) => Option.some(result)), Effect.timeoutOption(HANDOFF_TIMEOUT), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
141843
- if (Option.isNone(produced)) return yield* fallback("summary-unavailable");
141844
- if (produced.value.agentName !== P4_SUMMARIZER_AGENT.name && produced.value.agentName !== "summarizer") return yield* fallback("summarizer-agent-mismatch");
141845
- if (produced.value.requestedModel !== model.model) return yield* fallback("summarizer-model-mismatch");
141846
- if (!summaryIsUsable({
141847
- summary: produced.value.summary,
141848
- includedSequences: evidence.includedSequences
141849
- })) return yield* fallback("summary-uncited");
143166
+ })).pipe(Effect.map((result) => Option.some(result)), Effect.catchCause(() => Effect.succeed(Option.none())));
143167
+ /** The hardened identity checks, which a repair may never talk its way past. */
143168
+ const identityRejection = (result) => result.agentName !== P4_SUMMARIZER_AGENT.name && result.agentName !== "summarizer" ? "summarizer-agent-mismatch" : result.requestedModel !== model.model ? "summarizer-model-mismatch" : null;
143169
+ const noSummary = (reason) => ({
143170
+ produced: Option.none(),
143171
+ reason
143172
+ });
143173
+ const attempt = yield* Effect.gen(function* () {
143174
+ const first = yield* runSummary(handoffSummaryInstructions({ includedSequences: evidence.includedSequences }));
143175
+ if (Option.isNone(first)) return noSummary("summary-unavailable");
143176
+ const identity = identityRejection(first.value);
143177
+ if (identity !== null) return noSummary(identity);
143178
+ const rejection = summaryRejection({
143179
+ summary: first.value.summary,
143180
+ includedSequences: evidence.includedSequences
143181
+ });
143182
+ if (rejection === null) return {
143183
+ produced: first,
143184
+ reason: null
143185
+ };
143186
+ const repaired = yield* runSummary(handoffSummaryRepairInstructions({
143187
+ rejection,
143188
+ summary: first.value.summary,
143189
+ includedSequences: evidence.includedSequences
143190
+ }));
143191
+ if (Option.isNone(repaired)) return noSummary(rejection);
143192
+ const repairedIdentity = identityRejection(repaired.value);
143193
+ if (repairedIdentity !== null) return noSummary(repairedIdentity);
143194
+ const repairedRejection = summaryRejection({
143195
+ summary: repaired.value.summary,
143196
+ includedSequences: evidence.includedSequences
143197
+ });
143198
+ return repairedRejection === null ? {
143199
+ produced: repaired,
143200
+ reason: null
143201
+ } : noSummary(repairedRejection);
143202
+ }).pipe(Effect.timeoutOption(HANDOFF_TIMEOUT), Effect.map(Option.getOrElse(() => noSummary("summary-unavailable"))));
143203
+ const produced = attempt.produced;
143204
+ if (Option.isNone(produced)) return yield* fallback(attempt.reason ?? "summary-unavailable");
141850
143205
  const preparedAt = DateTime.formatIso(yield* DateTime.now);
141851
143206
  return {
141852
143207
  status: "summarized",
@@ -142133,6 +143488,25 @@ const make$3 = Effect.gen(function* () {
142133
143488
  */
142134
143489
  const triageSkipStreak = /* @__PURE__ */ new Map();
142135
143490
  /**
143491
+ * Consecutive status turns coalesced per pair. In-memory like the counters
143492
+ * above, and safe there: the classification is recomputed from the same
143493
+ * events on replay, so a restart costs at most one extra wake rather than a
143494
+ * wrong one.
143495
+ */
143496
+ const statusCoalesceStreak = /* @__PURE__ */ new Map();
143497
+ /**
143498
+ * The monitor tasks each pair currently has open.
143499
+ *
143500
+ * A progress row carries no task type, so the only thing that makes it a
143501
+ * wait rather than unclassified work is the `task.started` that named it -
143502
+ * and that row belongs to an earlier turn, outside the window this turn is
143503
+ * described over. Held per pair so an open watch stays recognisable across
143504
+ * the turns it reports through, and dropped the moment the task ends.
143505
+ * In-memory like the counters above, and safe the same way: a restart
143506
+ * forgets the watch and costs one extra wake rather than a wrong one.
143507
+ */
143508
+ const openMonitorTaskIds = /* @__PURE__ */ new Map();
143509
+ /**
142136
143510
  * Gate detection and human-override resolution act on the CURRENT persisted
142137
143511
  * gate state, so replaying them against historical events is wrong twice
142138
143512
  * over: an old plan event would open a fresh gate, and an old user message
@@ -142551,15 +143925,28 @@ const make$3 = Effect.gen(function* () {
142551
143925
  /**
142552
143926
  * What the builder did between two review boundaries, read from the event
142553
143927
  * window rather than remembered: whether supervisor advice started the turn,
142554
- * whether a human spoke into it, and whether any tool ran. A text-only
142555
- * builder question counts as idle on purpose: one or two such rounds pass
142556
- * unhindered, and a longer tool-free exchange is a debate the user should
142557
- * see rather than fund.
143928
+ * whether a human spoke into it, whether any tool ran, and whether anything
143929
+ * happened that a supervisor has to see. A text-only builder question counts
143930
+ * as idle on purpose: one or two such rounds pass unhindered, and a longer
143931
+ * tool-free exchange is a debate the user should see rather than fund.
143932
+ *
143933
+ * `waitMarked`, `changedState`, `failedTool` and `gateActivity` are read from
143934
+ * activity kinds and typed task classifications alone; nothing here inspects
143935
+ * prose. Only a live, named wait marks a turn as waiting: a wake scheduled
143936
+ * for a real time, or a Monitor-tool task and its own progress. A command, a
143937
+ * background shell and an unnamed task all count as work, because any of
143938
+ * them can change the repository, and every ending - a cancelled wake, a
143939
+ * completed task, a failure, a decline - is a result rather than a wait.
142558
143940
  */
142559
143941
  const describeBuilderTurn = (input) => {
142560
143942
  let startedByAdvice = false;
142561
143943
  let humanMessage = false;
142562
143944
  let ranTools = false;
143945
+ let changedState = false;
143946
+ let failedTool = false;
143947
+ let gateActivity = false;
143948
+ let waitMarked = false;
143949
+ const monitorTaskIds = input.monitorTaskIds;
142563
143950
  let summary;
142564
143951
  const assistantMessages = /* @__PURE__ */ new Map();
142565
143952
  for (const event of input.events) {
@@ -142573,12 +143960,39 @@ const make$3 = Effect.gen(function* () {
142573
143960
  if (event.payload.text.startsWith("[fusion-advice]")) startedByAdvice = true;
142574
143961
  else if (!isFusionProtocolText(event.payload.text)) humanMessage = true;
142575
143962
  }
142576
- if (event.type === "thread.activity-appended" && event.payload.threadId === input.implementerThreadId && event.payload.activity.kind.startsWith("tool.")) ranTools = true;
143963
+ if (event.type === "thread.activity-appended" && event.payload.threadId === input.implementerThreadId) {
143964
+ const activity = event.payload.activity;
143965
+ const activityPayload = activity.payload ?? {};
143966
+ const unsuccessful = activityPayload.status === "failed" || activityPayload.status === "declined";
143967
+ if (activity.kind.startsWith("tool.")) {
143968
+ ranTools = true;
143969
+ if (activity.kind === "tool.denied" || unsuccessful) failedTool = true;
143970
+ else changedState = true;
143971
+ } else if (activity.kind === "wake.scheduled") if (activityPayload.wakeAt === null || activityPayload.wakeAt === void 0) changedState = true;
143972
+ else waitMarked = true;
143973
+ else if (activity.kind.startsWith("task.")) {
143974
+ if ((unsuccessful || activity.kind === "task.completed") && activityPayload.taskId !== void 0) monitorTaskIds.delete(activityPayload.taskId);
143975
+ if (unsuccessful) failedTool = true;
143976
+ else if (activity.kind === "task.completed") changedState = true;
143977
+ else if (activity.kind === "task.started") {
143978
+ const taskType = activityPayload.taskType;
143979
+ if (taskType !== void 0 && MONITOR_TOOL_TASK_TYPES.has(taskType)) {
143980
+ waitMarked = true;
143981
+ if (activityPayload.taskId !== void 0) monitorTaskIds.add(activityPayload.taskId);
143982
+ } else if (taskType === void 0 || !INERT_TASK_TYPES.has(taskType)) changedState = true;
143983
+ } else if (activity.kind === "task.progress" && activityPayload.taskId !== void 0 && monitorTaskIds.has(activityPayload.taskId)) waitMarked = true;
143984
+ }
143985
+ }
143986
+ if ((event.type === "thread-pair.gate-opened" || event.type === "thread-pair.gate-advanced" || event.type === "thread-pair.gate-resolved") && event.payload.pairId === input.pairId) gateActivity = true;
142577
143987
  }
142578
143988
  return {
142579
143989
  startedByAdvice,
142580
143990
  humanMessage,
142581
143991
  ranTools,
143992
+ changedState,
143993
+ failedTool,
143994
+ gateActivity,
143995
+ waitMarked,
142582
143996
  summary
142583
143997
  };
142584
143998
  };
@@ -143158,7 +144572,7 @@ const make$3 = Effect.gen(function* () {
143158
144572
  });
143159
144573
  return;
143160
144574
  }
143161
- const idleCycles = turn.startedByAdvice && !turn.humanMessage && !turn.ranTools ? (idleAdviceCycles.get(currentPair.id) ?? 0) + 1 : 0;
144575
+ const idleCycles = turn.startedByAdvice && !turn.humanMessage && !turn.ranTools && !turn.waitMarked && !turn.changedState && !turn.failedTool ? (idleAdviceCycles.get(currentPair.id) ?? 0) + 1 : 0;
143162
144576
  idleAdviceCycles.set(currentPair.id, idleCycles);
143163
144577
  if (idleCycles >= 3) {
143164
144578
  idleAdviceCycles.delete(currentPair.id);
@@ -143170,6 +144584,18 @@ const make$3 = Effect.gen(function* () {
143170
144584
  });
143171
144585
  return;
143172
144586
  }
144587
+ const statusOnly = turn.startedByAdvice && !turn.humanMessage && turn.waitMarked && !turn.changedState && !turn.failedTool && !turn.gateActivity;
144588
+ const streak = statusCoalesceStreak.get(currentPair.id);
144589
+ const alreadyCounted = streak !== void 0 && completion.sequence <= streak.throughSequence;
144590
+ const coalesced = statusOnly ? alreadyCounted ? streak.count : (streak?.count ?? 0) + 1 : 0;
144591
+ if (statusOnly && coalesced < 5) {
144592
+ statusCoalesceStreak.set(currentPair.id, {
144593
+ count: coalesced,
144594
+ throughSequence: Math.max(streak?.throughSequence ?? 0, completion.sequence)
144595
+ });
144596
+ return;
144597
+ }
144598
+ statusCoalesceStreak.delete(currentPair.id);
143173
144599
  const triageSettings = yield* readTriageSettings;
143174
144600
  if (triageSettings !== null) {
143175
144601
  if (currentPair.triagedFromSequence !== null && completion.sequence <= currentPair.triagedFromSequence) return;
@@ -143256,13 +144682,18 @@ const make$3 = Effect.gen(function* () {
143256
144682
  if (pair.reviewExperiment) completions = completions.filter((completion) => completion.payload.state === "completed").slice(-1);
143257
144683
  let boundary = activationCompletion?.sequence ?? pair.lastReviewedImplementerSequence;
143258
144684
  for (const completion of completions.filter((candidate) => candidate.payload.state === "completed")) {
144685
+ const monitorTaskIds = openMonitorTaskIds.get(pair.id) ?? /* @__PURE__ */ new Set();
143259
144686
  const turn = describeBuilderTurn({
143260
144687
  events,
143261
144688
  implementerThreadId: pair.implementerThreadId,
144689
+ pairId: pair.id,
143262
144690
  afterSequence: boundary,
143263
144691
  throughSequence: completion.sequence,
143264
- turnId: completion.payload.turnId
144692
+ turnId: completion.payload.turnId,
144693
+ monitorTaskIds
143265
144694
  });
144695
+ if (monitorTaskIds.size === 0) openMonitorTaskIds.delete(pair.id);
144696
+ else openMonitorTaskIds.set(pair.id, monitorTaskIds);
143266
144697
  boundary = completion.sequence;
143267
144698
  yield* processReview(pair, completion, turn);
143268
144699
  }
@@ -144891,11 +146322,11 @@ const WorkspaceEntriesLayerLive = layer$17.pipe(Layer.provide(layer$70));
144891
146322
  const WorkspaceFileSystemLayerLive = layer$16.pipe(Layer.provide(layer$70), Layer.provide(WorkspaceEntriesLayerLive));
144892
146323
  const WorkspaceLayerLive = Layer.mergeAll(layer$70, WorkspaceEntriesLayerLive, WorkspaceFileSystemLayerLive);
144893
146324
  const ProjectFaviconResolverLayerLive = layer$65.pipe(Layer.provide(layer$70), Layer.provide(layer$66));
144894
- const AuthLayerLive = layer$114.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$118));
146325
+ const AuthLayerLive = layer$115.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$119));
144895
146326
  const ProviderRuntimeLayerLive = Layer.mergeAll(ProviderSessionReaperLive, ProviderHealthRefresherLive).pipe(Layer.provideMerge(ProviderLayerLive), Layer.provideMerge(OrchestrationLayerLive));
144896
- const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(layer$27), Layer.provideMerge(Layer.mergeAll(PullRequestServiceLive, CheckpointingLayerLive, SourceControlProviderRegistryLayerLive)), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderAuthServiceLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive, RoutedTaskRepositoryLive)), Layer.provideMerge(Layer.mergeAll(PersistenceLayerLive, layer$89)), Layer.provideMerge(layer$83), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(layer$60), Layer.provideMerge(Layer.mergeAll(layer$59, layer$72.pipe(Layer.provide(layer$85)), layer$74, layer$69)), Layer.provideMerge(layer$54), Layer.provideMerge(layer$55), Layer.provideMerge(layer$56), Layer.provideMerge(layer$57), Layer.provideMerge(layer$58)).pipe(Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(AntigravityInstallation.layer), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$62), Layer.provideMerge(JevWorkspaceAdvisorLive.pipe(Layer.provide(TypeSafeClientLive))), Layer.provideMerge(layer$93.pipe(Layer.provide(layer$118))), Layer.provideMerge(SupervisorSecretsLive.pipe(Layer.provide(layer$118))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$84), Layer.provideMerge(layer$68), Layer.provideMerge(remoteComputerUseBindingsLayer.pipe(Layer.provide(layer$89))), Layer.provideMerge(layer$77), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$5.pipe(Layer.provideMerge(LinearClientLive), Layer.provideMerge(Layer.mergeAll(LinearMcpClientLive, TrackerSecretsLive)), Layer.provideMerge(Layer.mergeAll(layer$90, layer$21).pipe(Layer.provideMerge(layer$91), Layer.provideMerge(layer$92))))), Layer.provideMerge(layer$118));
144897
- const UsageLayerLive = layer$25.pipe(Layer.provide(layer$93.pipe(Layer.provide(layer$118))));
144898
- const RuntimeDependenciesLive = layer$24.pipe(Layer.provideMerge(RuntimeCoreDependenciesLive)).pipe(Layer.provideMerge(UsageLayerLive), Layer.provideMerge(layer$12), Layer.provideMerge(layer$11), Layer.provideMerge(layer$10), Layer.provideMerge(layer$80), Layer.provideMerge(layer$82), Layer.provideMerge(layer$81), Layer.provide(layer$122));
146327
+ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(layer$27), Layer.provideMerge(Layer.mergeAll(PullRequestServiceLive, CheckpointingLayerLive, SourceControlProviderRegistryLayerLive)), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderAuthServiceLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive, RoutedTaskRepositoryLive)), Layer.provideMerge(Layer.mergeAll(PersistenceLayerLive, layer$89)), Layer.provideMerge(layer$83), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(layer$60), Layer.provideMerge(Layer.mergeAll(layer$59, layer$72.pipe(Layer.provide(layer$85)), layer$74, layer$69)), Layer.provideMerge(layer$54), Layer.provideMerge(layer$55), Layer.provideMerge(layer$56), Layer.provideMerge(layer$57), Layer.provideMerge(layer$58)).pipe(Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(AntigravityInstallation.layer), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$62), Layer.provideMerge(JevWorkspaceAdvisorLive.pipe(Layer.provide(TypeSafeClientLive))), Layer.provideMerge(layer$93.pipe(Layer.provide(layer$119))), Layer.provideMerge(SupervisorSecretsLive.pipe(Layer.provide(layer$119))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$84), Layer.provideMerge(layer$68), Layer.provideMerge(remoteComputerUseBindingsLayer.pipe(Layer.provide(layer$89))), Layer.provideMerge(layer$77), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$5.pipe(Layer.provideMerge(LinearClientLive), Layer.provideMerge(Layer.mergeAll(LinearMcpClientLive, TrackerSecretsLive)), Layer.provideMerge(Layer.mergeAll(layer$90, layer$21).pipe(Layer.provideMerge(layer$91), Layer.provideMerge(layer$92))))), Layer.provideMerge(layer$119));
146328
+ const UsageLayerLive = layer$25.pipe(Layer.provide(layer$93.pipe(Layer.provide(layer$119))));
146329
+ const RuntimeDependenciesLive = layer$24.pipe(Layer.provideMerge(RuntimeCoreDependenciesLive)).pipe(Layer.provideMerge(UsageLayerLive), Layer.provideMerge(layer$12), Layer.provideMerge(layer$11), Layer.provideMerge(layer$10), Layer.provideMerge(layer$80), Layer.provideMerge(layer$82), Layer.provideMerge(layer$81), Layer.provide(layer$123));
144899
146330
  /**
144900
146331
  * Hub asset sync.
144901
146332
  *
@@ -145547,7 +146978,7 @@ const serviceCommand = Command.make("service").pipe(Command.withDescription("Man
145547
146978
  ]));
145548
146979
  //#endregion
145549
146980
  //#region src/bin.ts
145550
- const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$122, FetchHttpClient.layer);
146981
+ const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$123, FetchHttpClient.layer);
145551
146982
  /**
145552
146983
  * Prints an update reminder before dispatching, at most once a day.
145553
146984
  *