@akira-tl/forgerelay 0.8.4 → 0.8.6

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +10 -10
  3. package/dist/activity/audit-store.js +44 -6
  4. package/dist/activity/mcp-query-tools.js +53 -36
  5. package/dist/activity/query-service.js +41 -11
  6. package/dist/cli.js +16 -17
  7. package/dist/composite-activity.js +124 -33
  8. package/dist/config.js +8 -13
  9. package/dist/lsp/test-support/server-fixture.js +7 -7
  10. package/dist/process-sessions.js +2 -2
  11. package/dist/remote-auth.js +11 -1
  12. package/dist/remote-mcp-connection-pool.js +90 -0
  13. package/dist/remote-transport.js +28 -14
  14. package/dist/remote-workspace-relay.js +203 -45
  15. package/dist/server.js +134 -64
  16. package/dist/skills.js +1 -1
  17. package/dist/subagents/profiles.js +1 -2
  18. package/dist/subagents/providers/adapters/pi.js +2 -2
  19. package/dist/subagents/providers/availability.js +2 -2
  20. package/dist/subagents/providers/path.js +4 -4
  21. package/dist/ui/.vite/manifest.json +32 -32
  22. package/dist/ui/activity-panel-app.html +3 -3
  23. package/dist/ui/assets/{activity-panel-app-E1ju2dqI.js → activity-panel-app-CUAN6zyW.js} +1 -1
  24. package/dist/ui/assets/{heavy-payload-CeW-n9w5.js → heavy-payload-CgzrutLm.js} +1 -1
  25. package/dist/ui/assets/{review-payload-B9CO298v.js → review-payload-BrLbezbq.js} +1 -1
  26. package/dist/ui/assets/{scrollbar-C2twAENW.js → scrollbar-CbhpdW05.js} +1 -1
  27. package/dist/ui/assets/workspace-app-Bhj96tsR.js +1 -0
  28. package/dist/ui/assets/{workspace-app-CwbJnb_w.js → workspace-app-CxwJuZyS.js} +1 -1
  29. package/dist/ui/assets/{workspace-app-BztEvZIC.js → workspace-app-D6UR0AFl.js} +3 -3
  30. package/dist/ui/assets/workspace-app-ldjBmCJR.css +1 -0
  31. package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +1 -0
  32. package/dist/ui/workspace-app.html +4 -4
  33. package/dist/ui/workspace-lifecycle-app.html +4 -4
  34. package/dist/user-config.js +3 -19
  35. package/dist/workspace-presentation.js +69 -0
  36. package/docs/agent-profile-schema.md +4 -9
  37. package/docs/artifact-exchange.md +2 -1
  38. package/docs/chatgpt-coding-workflow.md +9 -9
  39. package/docs/configuration.md +20 -29
  40. package/docs/gotchas.md +10 -7
  41. package/docs/roadmap.md +6 -3
  42. package/docs/security.md +3 -2
  43. package/docs/setup.md +8 -5
  44. package/docs/versioning.md +1 -1
  45. package/package.json +6 -2
  46. package/scripts/debug/accept.mjs +7 -1
  47. package/scripts/debug/relay-accept.mjs +889 -0
  48. package/scripts/debug/runtime.mjs +0 -4
  49. package/scripts/debug/runtime.test.mjs +0 -2
  50. package/scripts/debug/traffic/run.sh +5 -0
  51. package/scripts/debug/traffic/traffic-audit.mjs +907 -0
  52. package/scripts/release/push-ready.mjs +124 -0
  53. package/scripts/release/push-ready.test.mjs +108 -0
  54. package/scripts/release/release-gate.test.mjs +1 -0
  55. package/scripts/release-proof.mjs +16 -1
  56. package/scripts/wiki/sync.mjs +246 -0
  57. package/dist/ui/assets/workspace-app-YnUST8IP.css +0 -1
  58. package/dist/ui/assets/workspace-app-rKuhdae8.js +0 -1
  59. package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.6] - 2026-09-02
8
+
9
+ ### Added
10
+
11
+ - Added `release:push-ready` as the canonical release-ready branch push, atomically advancing the remote main/release refs and synchronizing the local `main` only after divergence safety checks pass.
12
+
13
+ ### Changed
14
+
15
+ - Retired rename-era DevSpace public compatibility adapters that are well beyond the migration window: `DEVSPACE_*` environment fallbacks, automatic `~/.devspace` config/state/worktree reuse, `.devspace/agents` profile discovery, and old DevSpace package-path special cases. Persisted storage/protocol identifiers remain compatible where renaming them would orphan existing state.
16
+
17
+ ### Fixed
18
+
19
+ - Reduced Activity Panel wire amplification with state-only collapsed polling, lazy revisioned Activity indexes, cursor-based Bash output, adaptive polling, and persistent Relay connections.
20
+ - Restored the Workspace overview without reintroducing large repeated payloads by keeping Skills, instructions, and agent presentation metadata bounded.
21
+
22
+ ## [0.8.5] - 2026-08-31
23
+
24
+ ### Added
25
+
26
+ - Added source-controlled GitHub Wiki pages with validation and publishing tooling.
27
+
28
+ ### Changed
29
+
30
+ - Relayed checkout and managed-worktree Workspaces now preserve stable Gateway identity and Execution-owned Task/lifecycle semantics across close, reopen, delete, inspection, and Composite member routing.
31
+
32
+ ### Fixed
33
+
34
+ - Task reminders and relayed/Composite runtime state now survive independent MCP transport sessions instead of resetting or presenting stale session-local state.
35
+ - Managed-worktree Relay acceptance now tolerates Git-native LF/CRLF checkout normalization, keeping the release matrix portable across Linux, macOS, and Windows.
36
+
7
37
  ## [0.8.4] - 2026-08-31
8
38
 
9
39
  ### Added
package/README.md CHANGED
@@ -255,7 +255,7 @@ See [Agent Profile Schema](docs/agent-profile-schema.md) for the profile format.
255
255
 
256
256
  ## Configuration and upgrades from DevSpace
257
257
 
258
- New configuration uses the `FORGERELAY_*` prefix. For example:
258
+ Configuration uses the `FORGERELAY_*` prefix. For example:
259
259
 
260
260
  ```bash
261
261
  FORGERELAY_ALLOWED_ROOTS="$HOME/projects" \
@@ -263,16 +263,14 @@ FORGERELAY_PUBLIC_BASE_URL="https://forge.example.com" \
263
263
  forgerelay serve
264
264
  ```
265
265
 
266
- Existing `DEVSPACE_*` variables are still accepted as fallbacks during the
267
- rename transition. When both names are present, `FORGERELAY_*` wins.
266
+ The rename-era automatic compatibility window has ended. `DEVSPACE_*`
267
+ environment variables are ignored, ForgeRelay no longer auto-selects
268
+ `~/.devspace`, and project `.devspace/agents` profiles are not discovered.
268
269
 
269
- The same rule applies to persisted configuration. ForgeRelay prefers
270
- `~/.forgerelay`, but if that directory does not exist and an existing
271
- `~/.devspace` setup does, ForgeRelay keeps using the legacy directory rather
272
- than orphaning OAuth credentials, workspace state, or managed worktrees.
273
-
274
- Project-level `.devspace/agents` profiles are also still readable for migration
275
- compatibility. New project configuration should use `.forgerelay`.
270
+ If an older installation still has data under DevSpace-named directories,
271
+ migrate it explicitly or point the canonical ForgeRelay settings at those paths
272
+ while moving the data. Persisted internal identifiers that would otherwise
273
+ orphan existing state remain compatible and are not product-facing configuration.
276
274
 
277
275
  See [Configuration Reference](docs/configuration.md) for all supported options.
278
276
 
@@ -348,6 +346,7 @@ Trusted Publishing setup.
348
346
 
349
347
  ## Documentation
350
348
 
349
+ - [GitHub Wiki](https://github.com/Akira-TL/forgerelay/wiki) — 面向用户的使用指南、概念说明与故障排查入口。
351
350
  - [Setup Guide](docs/setup.md)
352
351
  - [Local Debugging and 7677 Acceptance](docs/debugging.md)
353
352
  - [ChatGPT Coding Workflow](docs/chatgpt-coding-workflow.md)
@@ -358,6 +357,7 @@ Trusted Publishing setup.
358
357
  - [Troubleshooting](docs/gotchas.md)
359
358
  - [Roadmap](docs/roadmap.md)
360
359
  - [Versioning and Release Management](docs/versioning.md)
360
+ - [GitHub Wiki Maintenance](docs/maintenance/wiki.md)
361
361
  - [Changelog](CHANGELOG.md)
362
362
  - [Attribution Notice](NOTICE.md)
363
363
 
@@ -67,13 +67,51 @@ export class ActivityAuditStore {
67
67
  });
68
68
  }
69
69
  turnRevision(turnId) {
70
- const row = this.database.sqlite.prepare(`select count(*) as revision
71
- from activity_audit_events
72
- where activity_id in (
73
- select activity_id from activity_audit_events
70
+ return this.turnState(turnId).revision;
71
+ }
72
+ turnState(turnId) {
73
+ const row = this.database.sqlite.prepare(`with turn_activities as (
74
+ select activity_id
75
+ from activity_audit_events
74
76
  where event_type = 'started' and turn_id = ?
75
- )`).get(turnId);
76
- return row.revision;
77
+ ), turn_events as (
78
+ select event.rowid, event.activity_id, event.event_type
79
+ from activity_audit_events event
80
+ join turn_activities turn_activity on turn_activity.activity_id = event.activity_id
81
+ ), latest_events as (
82
+ select event.event_type
83
+ from turn_events event
84
+ join (
85
+ select activity_id, max(rowid) as rowid
86
+ from turn_events
87
+ group by activity_id
88
+ ) latest on latest.rowid = event.rowid
89
+ )
90
+ select
91
+ (select count(*) from turn_events) as revision,
92
+ count(*) as total,
93
+ coalesce(sum(case when event_type = 'started' then 1 else 0 end), 0) as working,
94
+ coalesce(sum(case when event_type in ('failed', 'blocked') then 1 else 0 end), 0) as error
95
+ from latest_events`).get(turnId);
96
+ return row;
97
+ }
98
+ activityIdsSinceTurnRevision(turnId, knownRevision) {
99
+ const offset = Math.max(0, Math.floor(knownRevision));
100
+ const rows = this.database.sqlite.prepare(`select activity_id, min(rowid) as first_rowid
101
+ from (
102
+ select event.rowid, event.activity_id
103
+ from activity_audit_events event
104
+ where event.activity_id in (
105
+ select activity_id
106
+ from activity_audit_events
107
+ where event_type = 'started' and turn_id = ?
108
+ )
109
+ order by event.rowid asc
110
+ limit -1 offset ?
111
+ )
112
+ group by activity_id
113
+ order by first_rowid asc`).all(turnId, offset);
114
+ return rows.map((row) => row.activity_id);
77
115
  }
78
116
  getActivity(activityId) {
79
117
  const events = this.listEvents(activityId);
@@ -35,13 +35,15 @@ const activitySummarySchema = z.object({
35
35
  error: z.number().int().nonnegative(),
36
36
  }).optional(),
37
37
  });
38
- const snapshotOutputSchema = {
38
+ const stateOutputSchema = {
39
39
  turnId: z.string(),
40
40
  revision: z.number().int().nonnegative(),
41
41
  changed: z.boolean(),
42
42
  state: z.enum(["working", "done", "error"]),
43
+ };
44
+ const indexOutputSchema = {
45
+ ...stateOutputSchema,
43
46
  activities: z.array(activitySummarySchema),
44
- [ACTIVITY_PANEL_WORKSPACE_META_KEY]: z.record(z.string(), z.unknown()).optional(),
45
47
  };
46
48
  export function registerActivityQueryTools(server, queries, connectionScopeId, panelMeta = {}, panelDefaultExpanded = false, logging, workspacePanelState, relay) {
47
49
  const panelUi = typeof panelMeta.ui === "object" && panelMeta.ui !== null
@@ -53,7 +55,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
53
55
  inputSchema: {
54
56
  workspaceId: z.string().min(1).describe("Workspace identifier returned by open_workspace for the project work in this Host Turn."),
55
57
  },
56
- outputSchema: snapshotOutputSchema,
58
+ outputSchema: stateOutputSchema,
57
59
  _meta: {
58
60
  ...panelMeta,
59
61
  ui: {
@@ -80,10 +82,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
80
82
  [ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
81
83
  [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
82
84
  },
83
- structuredContent: {
84
- ...(relayed.structuredContent ?? {}),
85
- [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
86
- },
87
85
  };
88
86
  }
89
87
  const snapshot = queries.beginTurn(conversationScopeId, workspaceId);
@@ -92,7 +90,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
92
90
  turnId: snapshot.turnId,
93
91
  revision: snapshot.revision,
94
92
  state: snapshot.state,
95
- activities: snapshot.activities.length,
96
93
  workspaceId,
97
94
  transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
98
95
  });
@@ -106,21 +103,18 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
106
103
  type: "text",
107
104
  text: `Started ForgeRelay Panel Host Turn ${snapshot.turnId} for ${workspaceId}.`,
108
105
  }],
109
- structuredContent: {
110
- ...snapshot,
111
- [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
112
- },
106
+ structuredContent: { ...snapshot },
113
107
  };
114
108
  });
115
109
  registerAppTool(server, "activity_snapshot", {
116
110
  title: "Read Activity snapshot",
117
- description: "App-only data source for lightweight Activity summaries in one durable Host Turn.",
111
+ description: "App-only state source for one durable Host Turn. Returns only revision/change/state; Activity rows are fetched separately through activity_index when the Panel is expanded.",
118
112
  inputSchema: {
119
113
  turnId: z.string().optional().describe("Existing Host Turn identifier. Omit only for initial App bootstrap; ForgeRelay then resolves the current Host Turn from conversation metadata."),
120
114
  workspaceId: z.string().min(1).optional().describe("Workspace identifier used by the App during initial bootstrap so ForgeRelay can restore the Workspace section."),
121
115
  knownRevision: z.number().int().nonnegative().optional(),
122
116
  },
123
- outputSchema: snapshotOutputSchema,
117
+ outputSchema: stateOutputSchema,
124
118
  _meta: { ui: { visibility: ["app"] } },
125
119
  annotations: READ_ONLY_ANNOTATIONS,
126
120
  }, async ({ turnId, workspaceId, knownRevision }, extra) => {
@@ -139,10 +133,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
139
133
  [ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
140
134
  [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
141
135
  },
142
- structuredContent: {
143
- ...(relayed.structuredContent ?? {}),
144
- [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
145
- },
146
136
  }
147
137
  : relayed;
148
138
  }
@@ -150,7 +140,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
150
140
  if (!resolvedTurnId) {
151
141
  throw new Error("Activity snapshot bootstrap could not resolve the current Host Turn from conversation and workspace metadata.");
152
142
  }
153
- const snapshot = queries.snapshot(resolvedTurnId, knownRevision);
143
+ const snapshot = queries.state(resolvedTurnId, knownRevision);
154
144
  if (logging) {
155
145
  logEvent(logging, "debug", "activity_snapshot_call", {
156
146
  turnId: resolvedTurnId,
@@ -159,7 +149,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
159
149
  revision: snapshot.revision,
160
150
  changed: snapshot.changed,
161
151
  state: snapshot.state,
162
- activities: snapshot.activities.length,
163
152
  transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
164
153
  });
165
154
  }
@@ -172,16 +161,40 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
172
161
  [ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
173
162
  ...(workspace ? { [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace } : {}),
174
163
  },
175
- content: [{
176
- type: "text",
177
- text: snapshot.changed
178
- ? `Activity snapshot ${resolvedTurnId} revision ${snapshot.revision}.`
179
- : `Activity snapshot ${resolvedTurnId} unchanged at revision ${snapshot.revision}.`,
180
- }],
181
- structuredContent: {
182
- ...snapshot,
183
- ...(workspace ? { [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace } : {}),
184
- },
164
+ content: [],
165
+ structuredContent: { ...snapshot },
166
+ };
167
+ });
168
+ registerAppTool(server, "activity_index", {
169
+ title: "Read Activity index",
170
+ description: "App-only lazy Activity-row source for an expanded Panel. Pass knownRevision to receive only rows changed since the previous index read.",
171
+ inputSchema: {
172
+ turnId: z.string(),
173
+ knownRevision: z.number().int().nonnegative().optional(),
174
+ },
175
+ outputSchema: indexOutputSchema,
176
+ _meta: { ui: { visibility: ["app"] } },
177
+ annotations: READ_ONLY_ANNOTATIONS,
178
+ }, async ({ turnId, knownRevision }, extra) => {
179
+ const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
180
+ const relayed = await relay?.index(turnId, knownRevision, conversationScopeId);
181
+ if (relayed)
182
+ return relayed;
183
+ const index = queries.index(turnId, knownRevision);
184
+ if (logging) {
185
+ logEvent(logging, "debug", "activity_index_call", {
186
+ turnId,
187
+ knownRevision,
188
+ revision: index.revision,
189
+ changed: index.changed,
190
+ state: index.state,
191
+ activities: index.activities.length,
192
+ transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
193
+ });
194
+ }
195
+ return {
196
+ content: [],
197
+ structuredContent: { ...index },
185
198
  };
186
199
  });
187
200
  registerAppTool(server, "activity_detail", {
@@ -216,16 +229,17 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
216
229
  });
217
230
  }
218
231
  return {
219
- content: [{ type: "text", text: `Activity detail ${activityId}.` }],
232
+ content: [],
220
233
  structuredContent: { ...detail },
221
234
  };
222
235
  });
223
236
  registerAppTool(server, "activity_output", {
224
237
  title: "Read Bash output",
225
- description: "App-only lazy data source for complete durable Bash command/output by stable outputId.",
238
+ description: "App-only lazy data source for durable Bash command/output by stable outputId. Pass the returned cursor on follow-up reads to receive only newly appended output.",
226
239
  inputSchema: {
227
240
  turnId: z.string(),
228
241
  outputId: z.string(),
242
+ cursor: z.number().int().nonnegative().optional(),
229
243
  },
230
244
  outputSchema: {
231
245
  outputId: z.string(),
@@ -233,6 +247,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
233
247
  processId: z.number().int().positive(),
234
248
  command: z.string(),
235
249
  output: z.string(),
250
+ cursor: z.number().int().nonnegative(),
236
251
  status: z.enum(["running", "done", "failed"]),
237
252
  exitCode: z.number().int().optional(),
238
253
  signal: z.string().optional(),
@@ -242,16 +257,18 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
242
257
  },
243
258
  _meta: { ui: { visibility: ["app"] } },
244
259
  annotations: READ_ONLY_ANNOTATIONS,
245
- }, async ({ turnId, outputId }, extra) => {
260
+ }, async ({ turnId, outputId, cursor }, extra) => {
246
261
  const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
247
- const relayed = await relay?.output(turnId, outputId, conversationScopeId);
262
+ const relayed = await relay?.output(turnId, outputId, conversationScopeId, cursor);
248
263
  if (relayed)
249
264
  return relayed;
250
- const output = queries.bashOutput(turnId, outputId);
265
+ const output = queries.bashOutput(turnId, outputId, cursor);
251
266
  if (logging) {
252
267
  logEvent(logging, "debug", "activity_output_call", {
253
268
  turnId,
254
269
  outputId,
270
+ cursor,
271
+ nextCursor: output.cursor,
255
272
  activityId: output.activityId,
256
273
  processId: output.processId,
257
274
  status: output.status,
@@ -260,7 +277,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
260
277
  });
261
278
  }
262
279
  return {
263
- content: [{ type: "text", text: `Bash output ${outputId}.` }],
280
+ content: [],
264
281
  structuredContent: { ...output },
265
282
  };
266
283
  });
@@ -9,25 +9,50 @@ export class ActivityQueryService {
9
9
  }
10
10
  beginTurn(conversationScopeId, workspaceId) {
11
11
  const turn = this.turns.begin(conversationScopeId, workspaceId);
12
- return this.snapshot(turn.turnId);
12
+ return this.state(turn.turnId);
13
13
  }
14
14
  currentTurnId(conversationScopeId, workspaceId) {
15
15
  return this.turns.current(conversationScopeId, workspaceId)?.turnId;
16
16
  }
17
- snapshot(turnId, knownRevision) {
17
+ state(turnId, knownRevision) {
18
18
  this.requireTurn(turnId);
19
- const revision = this.audit.turnRevision(turnId);
20
- const activities = this.summaries(turnId);
21
- const state = aggregateState(activities);
22
- const changed = knownRevision === undefined || knownRevision !== revision;
19
+ const aggregate = this.audit.turnState(turnId);
20
+ const state = aggregate.total === 0 || aggregate.working > 0
21
+ ? "working"
22
+ : aggregate.error > 0
23
+ ? "error"
24
+ : "done";
23
25
  return {
24
26
  turnId,
25
- revision,
26
- changed,
27
+ revision: aggregate.revision,
28
+ changed: knownRevision === undefined || knownRevision !== aggregate.revision,
27
29
  state,
28
- activities: changed ? activities : [],
29
30
  };
30
31
  }
32
+ index(turnId, knownRevision) {
33
+ const state = this.state(turnId, knownRevision);
34
+ if (!state.changed)
35
+ return { ...state, activities: [] };
36
+ const activities = this.summaries(turnId);
37
+ if (knownRevision === undefined || knownRevision < 0 || knownRevision > state.revision) {
38
+ return { ...state, activities };
39
+ }
40
+ const changedIds = new Set(this.audit.activityIdsSinceTurnRevision(turnId, knownRevision));
41
+ if (changedIds.size === 0)
42
+ return { ...state, activities: [] };
43
+ for (const activity of activities) {
44
+ if (changedIds.has(activity.activityId) && activity.parentActivityId) {
45
+ changedIds.add(activity.parentActivityId);
46
+ }
47
+ }
48
+ return {
49
+ ...state,
50
+ activities: activities.filter((activity) => changedIds.has(activity.activityId)),
51
+ };
52
+ }
53
+ snapshot(turnId, knownRevision) {
54
+ return this.index(turnId, knownRevision);
55
+ }
31
56
  detail(turnId, activityId) {
32
57
  this.requireTurn(turnId);
33
58
  const record = this.audit.getActivity(activityId);
@@ -46,7 +71,7 @@ export class ActivityQueryService {
46
71
  ...(record.error !== undefined ? { error: record.error } : {}),
47
72
  };
48
73
  }
49
- bashOutput(turnId, outputId) {
74
+ bashOutput(turnId, outputId, cursor = 0) {
50
75
  this.requireTurn(turnId);
51
76
  const output = this.outputs.read(outputId);
52
77
  if (!output)
@@ -56,12 +81,17 @@ export class ActivityQueryService {
56
81
  if (!visible) {
57
82
  throw new Error(`Bash output ${outputId} is not part of Host Turn ${turnId}.`);
58
83
  }
84
+ const latestCursor = output.chunks.at(-1)?.sequence ?? 0;
59
85
  return {
60
86
  outputId: output.outputId,
61
87
  activityId: output.activityId,
62
88
  processId: output.processId,
63
89
  command: output.command,
64
- output: output.output,
90
+ output: output.chunks
91
+ .filter((chunk) => chunk.sequence > cursor)
92
+ .map((chunk) => chunk.data)
93
+ .join(""),
94
+ cursor: latestCursor,
65
95
  status: output.status,
66
96
  ...(output.exitCode !== undefined ? { exitCode: output.exitCode } : {}),
67
97
  ...(output.signal !== undefined ? { signal: output.signal } : {}),
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { SubagentDeliveryMailbox } from "./subagents/sessions/delivery-mailbox.j
17
17
  import { SubagentSessionManager } from "./subagents/sessions/manager.js";
18
18
  import { formatSubagentProviderAvailabilitySummary } from "./subagents/providers/availability.js";
19
19
  import { parseSubagentRunArgs } from "./subagents/cli-target.js";
20
- import { ensureForgeRelayInstanceId, generateInstanceId, generateOwnerToken, loadDevspaceFiles, removeForgeRelayRemote, renameForgeRelayRemote, resolveSubagentsFlag, writeDevspaceAuth, writeDevspaceConfig, writeForgeRelayRemote, } from "./user-config.js";
20
+ import { ensureForgeRelayInstanceId, generateInstanceId, generateOwnerToken, loadForgeRelayFiles, removeForgeRelayRemote, renameForgeRelayRemote, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, writeForgeRelayRemote, } from "./user-config.js";
21
21
  import { expandHomePath } from "./roots.js";
22
22
  import { shutdownHttpServer } from "./server-shutdown.js";
23
23
  import { publicEndpointUrl } from "./oauth/public-url.js";
@@ -72,12 +72,12 @@ function normalizeCommand(command) {
72
72
  throw new Error(`Unknown command: ${command}`);
73
73
  }
74
74
  async function ensureConfigured() {
75
- const files = loadDevspaceFiles();
75
+ const files = loadForgeRelayFiles();
76
76
  if (files.configExists && files.authExists) {
77
77
  ensureForgeRelayInstanceId();
78
78
  return;
79
79
  }
80
- if (process.env.FORGERELAY_OAUTH_OWNER_TOKEN ?? process.env.DEVSPACE_OAUTH_OWNER_TOKEN) {
80
+ if (process.env.FORGERELAY_OAUTH_OWNER_TOKEN) {
81
81
  ensureForgeRelayInstanceId();
82
82
  return;
83
83
  }
@@ -94,7 +94,7 @@ async function ensureConfigured() {
94
94
  await runInit({ force: false });
95
95
  }
96
96
  async function runInit({ force }) {
97
- const files = loadDevspaceFiles();
97
+ const files = loadForgeRelayFiles();
98
98
  if (!force && files.configExists && files.authExists) {
99
99
  prompts.log.info(`ForgeRelay is already configured at ${files.dir}`);
100
100
  prompts.log.info("Run `forgerelay init --force` to update it.");
@@ -155,8 +155,8 @@ async function runInit({ force }) {
155
155
  ownerToken: files.auth.ownerToken ?? generateOwnerToken(),
156
156
  instanceId: files.auth.instanceId ?? generateInstanceId(),
157
157
  };
158
- const configPath = writeDevspaceConfig(config);
159
- const authPath = writeDevspaceAuth(auth);
158
+ const configPath = writeForgeRelayConfig(config);
159
+ const authPath = writeForgeRelayAuth(auth);
160
160
  const lines = [
161
161
  `Config: ${configPath}`,
162
162
  `Auth: ${authPath}`,
@@ -285,8 +285,7 @@ async function resolveAuthOwnerToken(ownerToken) {
285
285
  }
286
286
  function localOwnerToken() {
287
287
  const token = process.env.FORGERELAY_OAUTH_OWNER_TOKEN
288
- ?? process.env.DEVSPACE_OAUTH_OWNER_TOKEN
289
- ?? loadDevspaceFiles().auth.ownerToken;
288
+ ?? loadForgeRelayFiles().auth.ownerToken;
290
289
  if (!token)
291
290
  throw new Error("ForgeRelay owner token is not configured on this machine.");
292
291
  return token;
@@ -302,7 +301,7 @@ async function runAuthCommand(args) {
302
301
  if (subcommand === "list") {
303
302
  if (rest.length > 0)
304
303
  throw new Error("forgerelay auth list does not accept additional arguments.");
305
- const remotes = loadDevspaceFiles().auth.remotes ?? {};
304
+ const remotes = loadForgeRelayFiles().auth.remotes ?? {};
306
305
  if (Object.keys(remotes).length === 0) {
307
306
  console.log("No remote ForgeRelay instances registered.");
308
307
  return;
@@ -333,7 +332,7 @@ async function runAuthCommand(args) {
333
332
  const [alias, ...extra] = rest;
334
333
  if (!alias || extra.length > 0)
335
334
  throw new Error("Usage: forgerelay auth test <alias>");
336
- const files = loadDevspaceFiles();
335
+ const files = loadForgeRelayFiles();
337
336
  const storedRemote = files.auth.remotes?.[alias];
338
337
  if (!storedRemote)
339
338
  throw new Error(`Unknown remote alias: ${alias}`);
@@ -372,7 +371,7 @@ async function runAuthCommand(args) {
372
371
  target,
373
372
  ...(parsed.sshRoute ? { sshRoute: parsed.sshRoute } : {}),
374
373
  };
375
- const files = loadDevspaceFiles();
374
+ const files = loadForgeRelayFiles();
376
375
  const existingAlias = Object.entries(files.auth.remotes ?? {}).find(([, record]) => record.instanceId === remote.instanceId)?.[0];
377
376
  const defaultAlias = parsed.sshRoute
378
377
  ? defaultSshRouteAlias(parsed.sshRoute)
@@ -385,7 +384,7 @@ async function runAuthCommand(args) {
385
384
  console.log(`Authenticated remote ${alias} (${remote.instanceId}).`);
386
385
  }
387
386
  async function runDoctor() {
388
- const files = loadDevspaceFiles();
387
+ const files = loadForgeRelayFiles();
389
388
  console.log(`Config dir: ${files.dir}`);
390
389
  console.log(`Config file: ${files.configExists ? files.configPath : "missing"}`);
391
390
  console.log(`Auth file: ${files.authExists ? files.authPath : "missing"}`);
@@ -416,7 +415,7 @@ async function runDoctor() {
416
415
  }
417
416
  function runConfigCommand(args) {
418
417
  const [subcommand, key, ...rest] = args;
419
- const files = loadDevspaceFiles();
418
+ const files = loadForgeRelayFiles();
420
419
  if (!subcommand || subcommand === "get") {
421
420
  console.log(JSON.stringify(files.config, null, 2));
422
421
  return;
@@ -430,7 +429,7 @@ function runConfigCommand(args) {
430
429
  const value = rest.join(" ").trim();
431
430
  if (!value)
432
431
  throw new Error("Missing publicBaseUrl value.");
433
- writeDevspaceConfig({
432
+ writeForgeRelayConfig({
434
433
  ...files.config,
435
434
  publicBaseUrl: normalizeOptionalPublicBaseUrl(value),
436
435
  });
@@ -517,7 +516,7 @@ async function runAgentsRun(args) {
517
516
  const started = existing
518
517
  ? manager.resume({ sessionId: existing.id, prompt: parsed.prompt })
519
518
  : await manager.start({
520
- workspaceId: process.env.FORGERELAY_WORKSPACE_ID ?? process.env.DEVSPACE_WORKSPACE_ID,
519
+ workspaceId: process.env.FORGERELAY_WORKSPACE_ID,
521
520
  workspaceRoot,
522
521
  target: parsed.target,
523
522
  prompt: parsed.prompt,
@@ -609,11 +608,11 @@ function writeSubagentPromptFile(prompt) {
609
608
  return filePath;
610
609
  }
611
610
  function resolveCurrentWorkspaceRoot() {
612
- return resolve(process.env.FORGERELAY_WORKSPACE_ROOT ?? process.env.DEVSPACE_WORKSPACE_ROOT ?? process.cwd());
611
+ return resolve(process.env.FORGERELAY_WORKSPACE_ROOT ?? process.cwd());
613
612
  }
614
613
  function resolveCurrentWorkspaceScope() {
615
614
  return {
616
- workspaceId: process.env.FORGERELAY_WORKSPACE_ID ?? process.env.DEVSPACE_WORKSPACE_ID,
615
+ workspaceId: process.env.FORGERELAY_WORKSPACE_ID,
617
616
  workspaceRoot: resolveCurrentWorkspaceRoot(),
618
617
  };
619
618
  }