@p4code/cli 0.3.16 → 0.3.18

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
@@ -238,7 +238,7 @@ const make$91 = () => {
238
238
  const layer$82 = Layer.sync(NetService, make$91);
239
239
  //#endregion
240
240
  //#region package.json
241
- var version = "0.3.16";
241
+ var version = "0.3.18";
242
242
  //#endregion
243
243
  //#region src/config.ts
244
244
  /**
@@ -2173,11 +2173,46 @@ const OrchestrationSubscribeThreadInput = Schema$1.Struct({
2173
2173
  * Requests an explicit marker after the subscription has emitted its initial
2174
2174
  * snapshot or catch-up replay and before it begins emitting live events.
2175
2175
  */
2176
- requestCompletionMarker: Schema$1.optionalKey(Schema$1.Boolean)
2176
+ requestCompletionMarker: Schema$1.optionalKey(Schema$1.Boolean),
2177
+ /**
2178
+ * When provided, the fallback snapshot frame (sent when `afterSequence` is
2179
+ * missing or the catch-up gap is too large) is windowed to the last
2180
+ * `turnLimit` user-anchored turns and carries `page` metadata. Absent means
2181
+ * the fallback snapshot is the full thread, preserving pre-pagination client
2182
+ * behavior. Live events are unaffected either way.
2183
+ */
2184
+ turnLimit: Schema$1.optionalKey(PositiveInt)
2185
+ });
2186
+ Schema$1.Struct({
2187
+ turnLimit: Schema$1.optionalKey(PositiveInt),
2188
+ beforeCursor: Schema$1.optionalKey(TrimmedNonEmptyString)
2189
+ });
2190
+ /**
2191
+ * Page metadata for a windowed thread detail read. `beforeCursor` is opaque and
2192
+ * exclusive: passing it back returns the adjacent disjoint slice of older
2193
+ * turns. `null` means the thread is fully loaded below this page. The
2194
+ * `snapshotSequence` mirrors the top-level snapshot sequence so history pages
2195
+ * can be sequence-checked against live state before merging.
2196
+ */
2197
+ const OrchestrationThreadDetailPage = Schema$1.Struct({
2198
+ beforeCursor: Schema$1.NullOr(TrimmedNonEmptyString),
2199
+ hasMore: Schema$1.Boolean,
2200
+ snapshotSequence: NonNegativeInt,
2201
+ /**
2202
+ * Highest event sequence applied to THIS thread at page read time. The
2203
+ * global `snapshotSequence` advances with every thread's events, so a
2204
+ * client cannot wait for it via its per-thread subscription; this
2205
+ * thread-scoped watermark is reachable. A client merging an older page
2206
+ * must first have applied live events up to it — otherwise a streaming
2207
+ * turn outside the loaded window could have deltas replayed on top of
2208
+ * page content that already includes them, duplicating text.
2209
+ */
2210
+ threadSequence: Schema$1.optionalKey(NonNegativeInt)
2177
2211
  });
2178
2212
  const OrchestrationThreadDetailSnapshot = Schema$1.Struct({
2179
2213
  snapshotSequence: NonNegativeInt,
2180
- thread: OrchestrationThread
2214
+ thread: OrchestrationThread,
2215
+ page: Schema$1.optional(OrchestrationThreadDetailPage)
2181
2216
  });
2182
2217
  const ProjectCreateCommand = Schema$1.Struct({
2183
2218
  type: Schema$1.Literal("project.create"),
@@ -4709,6 +4744,10 @@ var EnvironmentAuthHttpApi = class extends HttpApiGroup.make("auth").add(HttpApi
4709
4744
  error: EnvironmentScopedOperationErrors
4710
4745
  }).middleware(EnvironmentAuthenticatedAuth)) {};
4711
4746
  const EnvironmentOrchestrationThreadSnapshotParams = Schema$1.Struct({ threadId: ThreadId });
4747
+ const EnvironmentOrchestrationThreadSnapshotQuery = {
4748
+ turnLimit: Schema$1.optional(Schema$1.FiniteFromString.check(Schema$1.isInt(), Schema$1.isGreaterThanOrEqualTo(1))),
4749
+ beforeCursor: Schema$1.optional(TrimmedNonEmptyString)
4750
+ };
4712
4751
  var EnvironmentOrchestrationHttpApi = class extends HttpApiGroup.make("orchestration").add(HttpApiEndpoint.get("snapshot", "/api/orchestration/snapshot", {
4713
4752
  headers: OptionalBearerHeaders,
4714
4753
  success: OrchestrationReadModel,
@@ -4720,6 +4759,7 @@ var EnvironmentOrchestrationHttpApi = class extends HttpApiGroup.make("orchestra
4720
4759
  }).middleware(EnvironmentAuthenticatedAuth)).add(HttpApiEndpoint.get("threadSnapshot", "/api/orchestration/threads/:threadId", {
4721
4760
  headers: OptionalBearerHeaders,
4722
4761
  params: EnvironmentOrchestrationThreadSnapshotParams,
4762
+ payload: EnvironmentOrchestrationThreadSnapshotQuery,
4723
4763
  success: OrchestrationThreadDetailSnapshot,
4724
4764
  error: EnvironmentOrchestrationThreadSnapshotErrors
4725
4765
  }).middleware(EnvironmentAuthenticatedAuth)).add(HttpApiEndpoint.post("dispatch", "/api/orchestration/dispatch", {
@@ -8812,7 +8852,13 @@ const ServerConfig = Schema$1.Struct({
8812
8852
  /** Whether shell subscriptions can emit an opt-in catch-up completion marker. */
8813
8853
  shellResumeCompletionMarker: Schema$1.optionalKey(Schema$1.Boolean),
8814
8854
  /** Whether thread subscriptions can emit an opt-in catch-up completion marker. */
8815
- threadResumeCompletionMarker: Schema$1.optionalKey(Schema$1.Boolean)
8855
+ threadResumeCompletionMarker: Schema$1.optionalKey(Schema$1.Boolean),
8856
+ /**
8857
+ * Whether thread detail reads accept a turn window (`turnLimit`/
8858
+ * `beforeCursor`) and return `page` metadata. Clients must not send window
8859
+ * fields to servers that don't advertise this.
8860
+ */
8861
+ threadSnapshotPagination: Schema$1.optionalKey(Schema$1.Boolean)
8816
8862
  });
8817
8863
  const ServerUpsertKeybindingReplaceTarget = Schema$1.Struct({
8818
8864
  key: KeybindingValue,
@@ -15774,6 +15820,21 @@ var _052_CompactHistoricalToolActivities_default = Effect.gen(function* () {
15774
15820
  `;
15775
15821
  });
15776
15822
  //#endregion
15823
+ //#region src/persistence/Migrations/053_ProjectionTurnsKeysetIndex.ts
15824
+ /**
15825
+ * Composite index for windowed thread detail reads. Pagination orders turns by
15826
+ * the stable keyset (requested_at, turn_id); the pre-existing
15827
+ * (thread_id, requested_at) index cannot serve the tiebreak order, forcing a
15828
+ * temp B-tree over all of a thread's turns before the page LIMIT applies.
15829
+ * With this index the candidates scan is genuinely bounded by the page size.
15830
+ */
15831
+ var _053_ProjectionTurnsKeysetIndex_default = Effect.gen(function* () {
15832
+ yield* (yield* SqlClient.SqlClient)`
15833
+ CREATE INDEX IF NOT EXISTS idx_projection_turns_thread_keyset
15834
+ ON projection_turns(thread_id, requested_at, turn_id)
15835
+ `;
15836
+ });
15837
+ //#endregion
15777
15838
  //#region src/persistence/Migrations.ts
15778
15839
  /**
15779
15840
  * MigrationsLive - Migration runner with inline loader
@@ -16054,6 +16115,11 @@ const migrationEntries = [
16054
16115
  52,
16055
16116
  "CompactHistoricalToolActivities",
16056
16117
  _052_CompactHistoricalToolActivities_default
16118
+ ],
16119
+ [
16120
+ 53,
16121
+ "ProjectionTurnsKeysetIndex",
16122
+ _053_ProjectionTurnsKeysetIndex_default
16057
16123
  ]
16058
16124
  ];
16059
16125
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -16562,7 +16628,19 @@ function projectActivityPayload(activity) {
16562
16628
  if (item) projectedData.item = item;
16563
16629
  if ("command" in data) projectedData.command = data.command;
16564
16630
  const input = asRecord$6(data.input);
16565
- if (input && "command" in input) projectedData.input = { command: input.command };
16631
+ if (input) {
16632
+ const projectedInput = {};
16633
+ if ("command" in input) projectedInput.command = input.command;
16634
+ if (payload.itemType === "collab_agent_tool_call") {
16635
+ for (const field of [
16636
+ "description",
16637
+ "prompt",
16638
+ "subagent_type",
16639
+ "model"
16640
+ ]) if (field in input) projectedInput[field] = input[field];
16641
+ }
16642
+ if (Object.keys(projectedInput).length > 0) projectedData.input = projectedInput;
16643
+ }
16566
16644
  const changedFiles = [];
16567
16645
  collectChangedFiles(data, changedFiles, /* @__PURE__ */ new Set(), 0);
16568
16646
  if (changedFiles.length > 0) projectedData.files = changedFiles.map((path) => ({ path }));
@@ -30156,6 +30234,37 @@ Schema$1.Struct({
30156
30234
  Schema$1.Struct({ threadId: ThreadId });
30157
30235
  Context.Service()("@p4code/cli/persistence/Services/ProjectionCheckpoints/ProjectionCheckpointRepository");
30158
30236
  //#endregion
30237
+ //#region src/orchestration/threadDetailCursor.ts
30238
+ function encodeThreadDetailPageCursor(cursor) {
30239
+ return Buffer.from(JSON.stringify({
30240
+ t: cursor.threadId,
30241
+ a: cursor.beforeAnchorAt,
30242
+ i: cursor.beforeTurnId
30243
+ })).toString("base64url");
30244
+ }
30245
+ /**
30246
+ * Returns null for anything that is not a well-formed cursor. Callers degrade
30247
+ * a malformed or foreign-thread cursor to a first-page request.
30248
+ */
30249
+ function decodeThreadDetailPageCursor(encoded) {
30250
+ let parsed;
30251
+ try {
30252
+ parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
30253
+ } catch {
30254
+ return null;
30255
+ }
30256
+ if (parsed === null || typeof parsed !== "object") return null;
30257
+ const record = parsed;
30258
+ if (typeof record.t !== "string" || record.t.length === 0) return null;
30259
+ if (typeof record.a !== "string") return null;
30260
+ if (typeof record.i !== "string") return null;
30261
+ return {
30262
+ threadId: record.t,
30263
+ beforeAnchorAt: record.a,
30264
+ beforeTurnId: record.i
30265
+ };
30266
+ }
30267
+ //#endregion
30159
30268
  //#region ../../packages/shared/src/sourceControl.ts
30160
30269
  const GITHUB_CHANGE_REQUEST_PRESENTATION = {
30161
30270
  icon: "github",
@@ -30852,6 +30961,24 @@ const ProjectionThreadSearchRow = Schema$1.Struct({
30852
30961
  const WorkspaceRootLookupInput = Schema$1.Struct({ workspaceRoot: Schema$1.String });
30853
30962
  const ProjectIdLookupInput = Schema$1.Struct({ projectId: ProjectId });
30854
30963
  const ThreadIdLookupInput = Schema$1.Struct({ threadId: ThreadId });
30964
+ const ThreadTurnWindowLookupInput = Schema$1.Struct({
30965
+ threadId: ThreadId,
30966
+ beforeAnchorAt: Schema$1.String,
30967
+ beforeTurnKey: Schema$1.String,
30968
+ userTurnLimit: Schema$1.Number,
30969
+ maxRawTurns: Schema$1.Number
30970
+ });
30971
+ const ProjectionTurnWindowRowSchema = Schema$1.Struct({
30972
+ anchorAt: Schema$1.String,
30973
+ turnKey: Schema$1.String
30974
+ });
30975
+ const ThreadTurnRangeLookupInput = Schema$1.Struct({
30976
+ threadId: ThreadId,
30977
+ minAnchorAt: Schema$1.String,
30978
+ minTurnKey: Schema$1.String,
30979
+ beforeAnchorAt: Schema$1.String,
30980
+ beforeTurnKey: Schema$1.String
30981
+ });
30855
30982
  const ProjectionProjectLookupRowSchema = ProjectionProjectDbRowSchema;
30856
30983
  const ProjectionThreadIdLookupRowSchema = Schema$1.Struct({ threadId: ThreadId });
30857
30984
  const ProjectionThreadCheckpointContextThreadRowSchema = Schema$1.Struct({
@@ -31769,6 +31896,115 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
31769
31896
  LIMIT ${BTW_CONTEXT_MESSAGE_LIMIT}
31770
31897
  ) AS bounded
31771
31898
  ORDER BY created_at ASC, message_id ASC
31899
+ `
31900
+ });
31901
+ const getThreadEventWatermarkRow = SqlSchema.findOneOption({
31902
+ Request: Schema$1.Struct({
31903
+ threadId: ThreadId,
31904
+ maxSequence: Schema$1.Number
31905
+ }),
31906
+ Result: Schema$1.Struct({ threadSequence: Schema$1.NullOr(Schema$1.Number) }),
31907
+ execute: ({ threadId, maxSequence }) => sql`
31908
+ SELECT MAX(sequence) AS "threadSequence"
31909
+ FROM orchestration_events
31910
+ WHERE aggregate_kind = 'thread'
31911
+ AND stream_id = ${threadId}
31912
+ AND sequence <= ${maxSequence}
31913
+ AND event_type IN (
31914
+ 'thread.message-sent',
31915
+ 'thread.proposed-plan-upserted',
31916
+ 'thread.activity-appended',
31917
+ 'thread.turn-diff-completed',
31918
+ 'thread.reverted',
31919
+ 'thread.session-set'
31920
+ )
31921
+ `
31922
+ });
31923
+ const listTurnWindowRows = SqlSchema.findAll({
31924
+ Request: ThreadTurnWindowLookupInput,
31925
+ Result: ProjectionTurnWindowRowSchema,
31926
+ execute: ({ threadId, beforeAnchorAt, beforeTurnKey, userTurnLimit, maxRawTurns }) => sql`
31927
+ WITH candidates AS (
31928
+ SELECT
31929
+ turns.requested_at AS anchor_at,
31930
+ COALESCE(turns.turn_id, '') AS turn_key,
31931
+ turns.pending_message_id
31932
+ FROM projection_turns AS turns
31933
+ WHERE turns.thread_id = ${threadId}
31934
+ AND (
31935
+ turns.requested_at < ${beforeAnchorAt}
31936
+ OR (
31937
+ turns.requested_at = ${beforeAnchorAt}
31938
+ AND COALESCE(turns.turn_id, '') < ${beforeTurnKey}
31939
+ )
31940
+ )
31941
+ ORDER BY turns.requested_at DESC, turns.turn_id DESC
31942
+ LIMIT ${maxRawTurns}
31943
+ ),
31944
+ walked AS (
31945
+ SELECT
31946
+ candidates.anchor_at,
31947
+ candidates.turn_key,
31948
+ CASE WHEN messages.role = 'user' THEN 1 ELSE 0 END AS is_user_turn,
31949
+ SUM(CASE WHEN messages.role = 'user' THEN 1 ELSE 0 END) OVER (
31950
+ ORDER BY candidates.anchor_at DESC, candidates.turn_key DESC
31951
+ ) AS user_turns_seen
31952
+ FROM candidates
31953
+ LEFT JOIN projection_thread_messages AS messages
31954
+ ON messages.message_id = candidates.pending_message_id
31955
+ )
31956
+ SELECT
31957
+ anchor_at AS "anchorAt",
31958
+ turn_key AS "turnKey"
31959
+ FROM walked
31960
+ WHERE user_turns_seen < ${userTurnLimit}
31961
+ OR (user_turns_seen = ${userTurnLimit} AND is_user_turn = 1)
31962
+ ORDER BY anchor_at ASC, turn_key ASC
31963
+ `
31964
+ });
31965
+ const listThreadMessageRowsByThreadWindow = SqlSchema.findAll({
31966
+ Request: ThreadTurnRangeLookupInput,
31967
+ Result: ProjectionThreadMessageDbRowSchema,
31968
+ execute: ({ threadId, minAnchorAt, minTurnKey, beforeAnchorAt, beforeTurnKey }) => sql`
31969
+ SELECT
31970
+ message_id AS "messageId",
31971
+ thread_id AS "threadId",
31972
+ turn_id AS "turnId",
31973
+ role,
31974
+ text,
31975
+ attachments_json AS "attachments",
31976
+ is_streaming AS "isStreaming",
31977
+ created_at AS "createdAt",
31978
+ updated_at AS "updatedAt"
31979
+ FROM projection_thread_messages
31980
+ WHERE thread_id = ${threadId}
31981
+ AND (
31982
+ turn_id IN (
31983
+ SELECT turn_id FROM projection_turns
31984
+ WHERE thread_id = ${threadId}
31985
+ AND turn_id IS NOT NULL
31986
+ AND (
31987
+ requested_at > ${minAnchorAt}
31988
+ OR (
31989
+ requested_at = ${minAnchorAt}
31990
+ AND turn_id >= ${minTurnKey}
31991
+ )
31992
+ )
31993
+ AND (
31994
+ requested_at < ${beforeAnchorAt}
31995
+ OR (
31996
+ requested_at = ${beforeAnchorAt}
31997
+ AND turn_id < ${beforeTurnKey}
31998
+ )
31999
+ )
32000
+ )
32001
+ OR (
32002
+ turn_id IS NULL
32003
+ AND created_at >= ${minAnchorAt}
32004
+ AND created_at < ${beforeAnchorAt}
32005
+ )
32006
+ )
32007
+ ORDER BY created_at ASC, message_id ASC
31772
32008
  `
31773
32009
  });
31774
32010
  const getBtwThreadRow = SqlSchema.findOneOption({
@@ -31789,6 +32025,54 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
31789
32025
  OR projects.deleted_at IS NULL
31790
32026
  )
31791
32027
  LIMIT 1
32028
+ `
32029
+ });
32030
+ const listThreadActivityRowsByThreadWindow = SqlSchema.findAll({
32031
+ Request: ThreadTurnRangeLookupInput,
32032
+ Result: ProjectionThreadActivityDbRowSchema,
32033
+ execute: ({ threadId, minAnchorAt, minTurnKey, beforeAnchorAt, beforeTurnKey }) => sql`
32034
+ SELECT
32035
+ activity_id AS "activityId",
32036
+ thread_id AS "threadId",
32037
+ turn_id AS "turnId",
32038
+ tone,
32039
+ kind,
32040
+ summary,
32041
+ payload_json AS "payload",
32042
+ sequence,
32043
+ created_at AS "createdAt"
32044
+ FROM projection_thread_activities
32045
+ WHERE thread_id = ${threadId}
32046
+ AND (
32047
+ turn_id IN (
32048
+ SELECT turn_id FROM projection_turns
32049
+ WHERE thread_id = ${threadId}
32050
+ AND turn_id IS NOT NULL
32051
+ AND (
32052
+ requested_at > ${minAnchorAt}
32053
+ OR (
32054
+ requested_at = ${minAnchorAt}
32055
+ AND turn_id >= ${minTurnKey}
32056
+ )
32057
+ )
32058
+ AND (
32059
+ requested_at < ${beforeAnchorAt}
32060
+ OR (
32061
+ requested_at = ${beforeAnchorAt}
32062
+ AND turn_id < ${beforeTurnKey}
32063
+ )
32064
+ )
32065
+ )
32066
+ OR (
32067
+ turn_id IS NULL
32068
+ AND created_at >= ${minAnchorAt}
32069
+ AND created_at < ${beforeAnchorAt}
32070
+ )
32071
+ )
32072
+ ORDER BY
32073
+ sequence ASC,
32074
+ created_at ASC,
32075
+ activity_id ASC
31792
32076
  `
31793
32077
  });
31794
32078
  const getFullThreadDiffContextRow = SqlSchema.findOneOption({
@@ -32358,12 +32642,18 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
32358
32642
  scheduledWakeAt: threadRow.value.scheduledWakeAt
32359
32643
  });
32360
32644
  });
32361
- const getThreadDetailById = (threadId) => Effect.gen(function* () {
32645
+ const getThreadDetailByIdBounded = (threadId, bounds) => Effect.gen(function* () {
32362
32646
  const [threadRow, messageRows, proposedPlanRows, activityRows, checkpointRows, turnRows, latestTurnRow, sessionRow] = yield* Effect.all([
32363
32647
  getActiveThreadRowById({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getThread:query", "ProjectionSnapshotQuery.getThreadDetailById:getThread:decodeRow"))),
32364
- listThreadMessageRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listMessages:query", "ProjectionSnapshotQuery.getThreadDetailById:listMessages:decodeRows"))),
32648
+ (bounds === void 0 ? listThreadMessageRowsByThread({ threadId }) : listThreadMessageRowsByThreadWindow({
32649
+ threadId,
32650
+ ...bounds
32651
+ })).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listMessages:query", "ProjectionSnapshotQuery.getThreadDetailById:listMessages:decodeRows"))),
32365
32652
  listThreadProposedPlanRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listPlans:query", "ProjectionSnapshotQuery.getThreadDetailById:listPlans:decodeRows"))),
32366
- listThreadActivityRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listActivities:query", "ProjectionSnapshotQuery.getThreadDetailById:listActivities:decodeRows"))),
32653
+ (bounds === void 0 ? listThreadActivityRowsByThread({ threadId }) : listThreadActivityRowsByThreadWindow({
32654
+ threadId,
32655
+ ...bounds
32656
+ })).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listActivities:query", "ProjectionSnapshotQuery.getThreadDetailById:listActivities:decodeRows"))),
32367
32657
  listCheckpointRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listCheckpoints:query", "ProjectionSnapshotQuery.getThreadDetailById:listCheckpoints:decodeRows"))),
32368
32658
  listTurnSummaryRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listTurnSummaries:query", "ProjectionSnapshotQuery.getThreadDetailById:listTurnSummaries:decodeRows"))),
32369
32659
  getLatestTurnRowByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getLatestTurn:query", "ProjectionSnapshotQuery.getThreadDetailById:getLatestTurn:decodeRow"))),
@@ -32434,13 +32724,70 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
32434
32724
  };
32435
32725
  return Option.some(yield* decodeThread(thread).pipe(Effect.mapError(toPersistenceDecodeError("ProjectionSnapshotQuery.getThreadDetailById:decodeThread"))));
32436
32726
  });
32437
- const getThreadDetailSnapshot = (threadId) => sql.withTransaction(Effect.gen(function* () {
32438
- const thread = yield* getThreadDetailById(threadId);
32727
+ const getThreadDetailById = (threadId) => getThreadDetailByIdBounded(threadId, void 0);
32728
+ const THREAD_DETAIL_MAX_RAW_TURNS_PER_PAGE = 150;
32729
+ const ANCHOR_UNBOUNDED = "~";
32730
+ const getThreadDetailSnapshot = (threadId, window) => sql.withTransaction(Effect.gen(function* () {
32731
+ if (window?.turnLimit === void 0) {
32732
+ const thread = yield* getThreadDetailById(threadId);
32733
+ if (Option.isNone(thread)) return Option.none();
32734
+ const { snapshotSequence } = yield* getSnapshotSequence();
32735
+ return Option.some({
32736
+ snapshotSequence,
32737
+ thread: thread.value
32738
+ });
32739
+ }
32740
+ const decodedCursor = window.beforeCursor === void 0 ? null : decodeThreadDetailPageCursor(window.beforeCursor);
32741
+ const cursor = decodedCursor?.threadId === threadId ? decodedCursor : null;
32742
+ const oldest = (yield* listTurnWindowRows({
32743
+ threadId,
32744
+ beforeAnchorAt: cursor?.beforeAnchorAt ?? ANCHOR_UNBOUNDED,
32745
+ beforeTurnKey: cursor?.beforeTurnId ?? "",
32746
+ userTurnLimit: window.turnLimit,
32747
+ maxRawTurns: THREAD_DETAIL_MAX_RAW_TURNS_PER_PAGE
32748
+ }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailSnapshot:listTurnWindow:query", "ProjectionSnapshotQuery.getThreadDetailSnapshot:listTurnWindow:decodeRows"))))[0];
32749
+ const bounds = oldest === void 0 && cursor === null ? void 0 : {
32750
+ minAnchorAt: oldest?.anchorAt ?? "",
32751
+ minTurnKey: oldest?.turnKey ?? "",
32752
+ beforeAnchorAt: cursor?.beforeAnchorAt ?? ANCHOR_UNBOUNDED,
32753
+ beforeTurnKey: cursor?.beforeTurnId ?? ""
32754
+ };
32755
+ const thread = yield* getThreadDetailByIdBounded(threadId, (oldest === void 0 && cursor !== null ? {
32756
+ minAnchorAt: "",
32757
+ minTurnKey: "",
32758
+ beforeAnchorAt: "",
32759
+ beforeTurnKey: ""
32760
+ } : void 0) ?? bounds);
32439
32761
  if (Option.isNone(thread)) return Option.none();
32762
+ const hasMore = oldest !== void 0 && (yield* listTurnWindowRows({
32763
+ threadId,
32764
+ beforeAnchorAt: oldest.anchorAt,
32765
+ beforeTurnKey: oldest.turnKey,
32766
+ userTurnLimit: 1,
32767
+ maxRawTurns: 1
32768
+ }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailSnapshot:probeOlder:query", "ProjectionSnapshotQuery.getThreadDetailSnapshot:probeOlder:decodeRows")))).length > 0;
32440
32769
  const { snapshotSequence } = yield* getSnapshotSequence();
32770
+ const watermarkRow = yield* getThreadEventWatermarkRow({
32771
+ threadId,
32772
+ maxSequence: snapshotSequence
32773
+ }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailSnapshot:threadWatermark:query", "ProjectionSnapshotQuery.getThreadDetailSnapshot:threadWatermark:decodeRow")));
32774
+ const threadSequence = Option.match(watermarkRow, {
32775
+ onNone: () => 0,
32776
+ onSome: (row) => row.threadSequence ?? 0
32777
+ });
32441
32778
  return Option.some({
32442
32779
  snapshotSequence,
32443
- thread: thread.value
32780
+ thread: thread.value,
32781
+ page: {
32782
+ beforeCursor: hasMore && oldest !== void 0 ? encodeThreadDetailPageCursor({
32783
+ threadId,
32784
+ beforeAnchorAt: oldest.anchorAt,
32785
+ beforeTurnId: oldest.turnKey
32786
+ }) : null,
32787
+ hasMore,
32788
+ snapshotSequence,
32789
+ threadSequence
32790
+ }
32444
32791
  });
32445
32792
  })).pipe(Effect.mapError((error) => isPersistenceError(error) ? error : toPersistenceSqlError("ProjectionSnapshotQuery.getThreadDetailSnapshot:transaction")(error)));
32446
32793
  return {
@@ -63445,7 +63792,8 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
63445
63792
  },
63446
63793
  settings,
63447
63794
  shellResumeCompletionMarker: true,
63448
- threadResumeCompletionMarker: true
63795
+ threadResumeCompletionMarker: true,
63796
+ threadSnapshotPagination: true
63449
63797
  };
63450
63798
  });
63451
63799
  const refreshGitStatus = (cwd) => vcsStatusBroadcaster.refreshStatus(cwd).pipe(Effect.ignoreCause({ log: true }), Effect.forkDetach, Effect.asVoid);
@@ -63561,7 +63909,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
63561
63909
  const afterCatchUp = input.requestCompletionMarker === true ? Stream.concat(Stream.fromEffect(Queue.offer(liveBuffer, { kind: "synchronized" })).pipe(Stream.drain), bufferedLiveStream) : bufferedLiveStream;
63562
63910
  return Stream.concat(catchUpStream, afterCatchUp);
63563
63911
  }
63564
- const snapshot = yield* projectionSnapshotQuery.getThreadDetailSnapshot(input.threadId).pipe(Effect.mapError((cause) => new OrchestrationGetSnapshotError({
63912
+ const snapshot = yield* projectionSnapshotQuery.getThreadDetailSnapshot(input.threadId, input.turnLimit === void 0 ? void 0 : { turnLimit: input.turnLimit }).pipe(Effect.mapError((cause) => new OrchestrationGetSnapshotError({
63565
63913
  message: `Failed to load thread ${input.threadId}`,
63566
63914
  cause
63567
63915
  })));
@@ -71904,7 +72252,8 @@ const ServerNotification__SpendControlLimitSnapshot = Schema$1.Struct({
71904
72252
  const ServerNotification__SubAgentActivityKind = Schema$1.Literals([
71905
72253
  "started",
71906
72254
  "interacted",
71907
- "interrupted"
72255
+ "interrupted",
72256
+ "completed"
71908
72257
  ]);
71909
72258
  const ServerNotification__TerminalInteractionNotification = Schema$1.Struct({
71910
72259
  itemId: Schema$1.String,
@@ -72791,7 +73140,8 @@ const V2ItemCompletedNotification__ReasoningEffort = Schema$1.String.annotate({
72791
73140
  const V2ItemCompletedNotification__SubAgentActivityKind = Schema$1.Literals([
72792
73141
  "started",
72793
73142
  "interacted",
72794
- "interrupted"
73143
+ "interrupted",
73144
+ "completed"
72795
73145
  ]);
72796
73146
  const V2ItemCompletedNotification__TextElement = Schema$1.Struct({
72797
73147
  byteRange: Schema$1.Struct({
@@ -72975,7 +73325,8 @@ const V2ItemStartedNotification__ReasoningEffort = Schema$1.String.annotate({ de
72975
73325
  const V2ItemStartedNotification__SubAgentActivityKind = Schema$1.Literals([
72976
73326
  "started",
72977
73327
  "interacted",
72978
- "interrupted"
73328
+ "interrupted",
73329
+ "completed"
72979
73330
  ]);
72980
73331
  const V2ItemStartedNotification__TextElement = Schema$1.Struct({
72981
73332
  byteRange: Schema$1.Struct({
@@ -73470,7 +73821,8 @@ const V2ReviewStartResponse__ReasoningEffort = Schema$1.String.annotate({ descri
73470
73821
  const V2ReviewStartResponse__SubAgentActivityKind = Schema$1.Literals([
73471
73822
  "started",
73472
73823
  "interacted",
73473
- "interrupted"
73824
+ "interrupted",
73825
+ "completed"
73474
73826
  ]);
73475
73827
  const V2ReviewStartResponse__TextElement = Schema$1.Struct({
73476
73828
  byteRange: Schema$1.Struct({
@@ -73655,7 +74007,8 @@ const V2ThreadForkResponse__ReasoningEffort = Schema$1.String.annotate({ descrip
73655
74007
  const V2ThreadForkResponse__SubAgentActivityKind = Schema$1.Literals([
73656
74008
  "started",
73657
74009
  "interacted",
73658
- "interrupted"
74010
+ "interrupted",
74011
+ "completed"
73659
74012
  ]);
73660
74013
  const V2ThreadForkResponse__TextElement = Schema$1.Struct({
73661
74014
  byteRange: Schema$1.Struct({
@@ -73837,7 +74190,8 @@ const V2ThreadListResponse__ReasoningEffort = Schema$1.String.annotate({ descrip
73837
74190
  const V2ThreadListResponse__SubAgentActivityKind = Schema$1.Literals([
73838
74191
  "started",
73839
74192
  "interacted",
73840
- "interrupted"
74193
+ "interrupted",
74194
+ "completed"
73841
74195
  ]);
73842
74196
  const V2ThreadListResponse__TextElement = Schema$1.Struct({
73843
74197
  byteRange: Schema$1.Struct({
@@ -73973,7 +74327,8 @@ const V2ThreadMetadataUpdateResponse__ReasoningEffort = Schema$1.String.annotate
73973
74327
  const V2ThreadMetadataUpdateResponse__SubAgentActivityKind = Schema$1.Literals([
73974
74328
  "started",
73975
74329
  "interacted",
73976
- "interrupted"
74330
+ "interrupted",
74331
+ "completed"
73977
74332
  ]);
73978
74333
  const V2ThreadMetadataUpdateResponse__TextElement = Schema$1.Struct({
73979
74334
  byteRange: Schema$1.Struct({
@@ -74104,7 +74459,8 @@ const V2ThreadReadResponse__ReasoningEffort = Schema$1.String.annotate({ descrip
74104
74459
  const V2ThreadReadResponse__SubAgentActivityKind = Schema$1.Literals([
74105
74460
  "started",
74106
74461
  "interacted",
74107
- "interrupted"
74462
+ "interrupted",
74463
+ "completed"
74108
74464
  ]);
74109
74465
  const V2ThreadReadResponse__TextElement = Schema$1.Struct({
74110
74466
  byteRange: Schema$1.Struct({
@@ -74346,7 +74702,8 @@ const V2ThreadResumeResponse__ReasoningEffort = Schema$1.String.annotate({ descr
74346
74702
  const V2ThreadResumeResponse__SubAgentActivityKind = Schema$1.Literals([
74347
74703
  "started",
74348
74704
  "interacted",
74349
- "interrupted"
74705
+ "interrupted",
74706
+ "completed"
74350
74707
  ]);
74351
74708
  const V2ThreadResumeResponse__TextElement = Schema$1.Struct({
74352
74709
  byteRange: Schema$1.Struct({
@@ -74477,7 +74834,8 @@ const V2ThreadRollbackResponse__ReasoningEffort = Schema$1.String.annotate({ des
74477
74834
  const V2ThreadRollbackResponse__SubAgentActivityKind = Schema$1.Literals([
74478
74835
  "started",
74479
74836
  "interacted",
74480
- "interrupted"
74837
+ "interrupted",
74838
+ "completed"
74481
74839
  ]);
74482
74840
  const V2ThreadRollbackResponse__TextElement = Schema$1.Struct({
74483
74841
  byteRange: Schema$1.Struct({
@@ -74641,7 +74999,8 @@ const V2ThreadStartedNotification__ReasoningEffort = Schema$1.String.annotate({
74641
74999
  const V2ThreadStartedNotification__SubAgentActivityKind = Schema$1.Literals([
74642
75000
  "started",
74643
75001
  "interacted",
74644
- "interrupted"
75002
+ "interrupted",
75003
+ "completed"
74645
75004
  ]);
74646
75005
  const V2ThreadStartedNotification__TextElement = Schema$1.Struct({
74647
75006
  byteRange: Schema$1.Struct({
@@ -74819,7 +75178,8 @@ const V2ThreadStartResponse__ReasoningEffort = Schema$1.String.annotate({ descri
74819
75178
  const V2ThreadStartResponse__SubAgentActivityKind = Schema$1.Literals([
74820
75179
  "started",
74821
75180
  "interacted",
74822
- "interrupted"
75181
+ "interrupted",
75182
+ "completed"
74823
75183
  ]);
74824
75184
  const V2ThreadStartResponse__TextElement = Schema$1.Struct({
74825
75185
  byteRange: Schema$1.Struct({
@@ -74962,7 +75322,8 @@ const V2ThreadUnarchiveResponse__ReasoningEffort = Schema$1.String.annotate({ de
74962
75322
  const V2ThreadUnarchiveResponse__SubAgentActivityKind = Schema$1.Literals([
74963
75323
  "started",
74964
75324
  "interacted",
74965
- "interrupted"
75325
+ "interrupted",
75326
+ "completed"
74966
75327
  ]);
74967
75328
  const V2ThreadUnarchiveResponse__TextElement = Schema$1.Struct({
74968
75329
  byteRange: Schema$1.Struct({
@@ -75092,7 +75453,8 @@ const V2TurnCompletedNotification__ReasoningEffort = Schema$1.String.annotate({
75092
75453
  const V2TurnCompletedNotification__SubAgentActivityKind = Schema$1.Literals([
75093
75454
  "started",
75094
75455
  "interacted",
75095
- "interrupted"
75456
+ "interrupted",
75457
+ "completed"
75096
75458
  ]);
75097
75459
  const V2TurnCompletedNotification__TextElement = Schema$1.Struct({
75098
75460
  byteRange: Schema$1.Struct({
@@ -75219,7 +75581,8 @@ const V2TurnStartedNotification__ReasoningEffort = Schema$1.String.annotate({ de
75219
75581
  const V2TurnStartedNotification__SubAgentActivityKind = Schema$1.Literals([
75220
75582
  "started",
75221
75583
  "interacted",
75222
- "interrupted"
75584
+ "interrupted",
75585
+ "completed"
75223
75586
  ]);
75224
75587
  const V2TurnStartedNotification__TextElement = Schema$1.Struct({
75225
75588
  byteRange: Schema$1.Struct({
@@ -75385,7 +75748,8 @@ const V2TurnStartResponse__ReasoningEffort = Schema$1.String.annotate({ descript
75385
75748
  const V2TurnStartResponse__SubAgentActivityKind = Schema$1.Literals([
75386
75749
  "started",
75387
75750
  "interacted",
75388
- "interrupted"
75751
+ "interrupted",
75752
+ "completed"
75389
75753
  ]);
75390
75754
  const V2TurnStartResponse__TextElement = Schema$1.Struct({
75391
75755
  byteRange: Schema$1.Struct({
@@ -90944,7 +91308,8 @@ const COLLAB_TOOL_TITLES = {
90944
91308
  const SUB_AGENT_ACTIVITY_TITLES = {
90945
91309
  started: "Agent started",
90946
91310
  interacted: "Agent worked",
90947
- interrupted: "Agent interrupted"
91311
+ interrupted: "Agent interrupted",
91312
+ completed: "Agent completed"
90948
91313
  };
90949
91314
  function agentNameFromPath(agentPath) {
90950
91315
  return trimText$1(agentPath.split("/").pop()?.replace(/\.[^.]+$/, ""));
@@ -90978,12 +91343,12 @@ function collabTaskEvents(event, canonicalThreadId, item, defaults) {
90978
91343
  } : {}
90979
91344
  }
90980
91345
  }];
90981
- if (item.kind === "interrupted") return [{
91346
+ if (item.kind === "interrupted" || item.kind === "completed") return [{
90982
91347
  ...collabTaskEventBase(base, taskId, "completed"),
90983
91348
  type: "task.completed",
90984
91349
  payload: {
90985
91350
  taskId,
90986
- status: "stopped"
91351
+ status: item.kind === "completed" ? "completed" : "stopped"
90987
91352
  }
90988
91353
  }];
90989
91354
  return [{
@@ -109784,7 +110149,10 @@ const orchestrationHttpApiLayer = HttpApiBuilder.group(EnvironmentHttpApi, "orch
109784
110149
  })).handle("threadSnapshot", Effect.fn("environment.orchestration.threadSnapshot")(function* (args) {
109785
110150
  yield* annotateEnvironmentRequest(args.endpoint.name);
109786
110151
  yield* requireEnvironmentScope(AuthOrchestrationReadScope);
109787
- const snapshot = yield* projectionSnapshotQuery.getThreadDetailSnapshot(args.params.threadId).pipe(Effect.catch((cause) => failEnvironmentInternal("orchestration_thread_snapshot_failed", cause)));
110152
+ const snapshot = yield* projectionSnapshotQuery.getThreadDetailSnapshot(args.params.threadId, args.payload.turnLimit === void 0 ? void 0 : {
110153
+ turnLimit: args.payload.turnLimit,
110154
+ ...args.payload.beforeCursor !== void 0 ? { beforeCursor: args.payload.beforeCursor } : {}
110155
+ }).pipe(Effect.catch((cause) => failEnvironmentInternal("orchestration_thread_snapshot_failed", cause)));
109788
110156
  if (Option.isNone(snapshot)) return yield* failEnvironmentNotFound("thread_not_found");
109789
110157
  return projectThreadDetailSnapshot(snapshot.value);
109790
110158
  })).handle("dispatch", Effect.fn("environment.orchestration.dispatch")(function* (args) {