@danceiny/gotry 0.2.0-rc.28 → 0.2.0-rc.30

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 (44) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/gotry-bootstrap.js +9 -9
  4. package/bin/gotry-inner.js +7 -1
  5. package/cordis.gotry-patch.yml +2 -0
  6. package/data/yunnan-pack.json +30 -27
  7. package/dist/capabilities/artifacts.js +41 -4
  8. package/dist/capabilities/channel-registry.js +4 -4
  9. package/dist/capabilities/doctor.js +3 -3
  10. package/dist/capabilities/effect.js +1 -1
  11. package/dist/capabilities/session/extension-bridge.js +3 -3
  12. package/dist/capabilities/session-search.js +1 -1
  13. package/dist/scripts/agent-planning-turn-deadline-e2e.js +2 -0
  14. package/dist/scripts/agent-planning-turn-deadline-tests.js +135 -2
  15. package/dist/scripts/bootstrap-tests.js +11 -6
  16. package/dist/scripts/doctor-tests.js +2 -2
  17. package/dist/scripts/gotry-backend-kernel-tests.js +1 -1
  18. package/dist/scripts/issue-271-liveness-tests.js +6 -0
  19. package/dist/scripts/persona-sim-tests.js +19 -1
  20. package/dist/scripts/persona-sim.js +1 -1
  21. package/dist/scripts/session-dida-live-e2e.js +1 -1
  22. package/dist/scripts/turn-handoff-collect-tests.js +7 -1
  23. package/dist/scripts/turn-handoff-collect.js +68 -20
  24. package/dist/scripts/turn-handoff-job-tests.js +222 -0
  25. package/dist/scripts/turn-handoff-web-e2e.js +240 -0
  26. package/dist/scripts/unified-tests.js +71 -30
  27. package/dist/src/backend/modules/session-search.js +1 -1
  28. package/dist/src/index.js +30 -14
  29. package/dist/src/turn-deadline.js +113 -16
  30. package/dist/src/turn-handoff-job.js +124 -0
  31. package/extension/README.md +1 -1
  32. package/extension/README.zh-CN.md +1 -1
  33. package/extension/background.js +1 -1
  34. package/extension/content-bridge.js +1 -1
  35. package/extension/content-main.js +1 -1
  36. package/extension/manifest.json +2 -2
  37. package/package.json +2 -1
  38. package/ts/capabilities/artifacts.ts +40 -5
  39. package/ts/capabilities/session/extension-bridge.ts +3 -3
  40. package/ts/capabilities/session-search.ts +1 -1
  41. package/ts/scripts/turn-handoff-collect.ts +63 -21
  42. package/ts/src/index.ts +30 -14
  43. package/ts/src/turn-deadline.ts +111 -18
  44. package/ts/src/turn-handoff-job.ts +120 -0
@@ -0,0 +1,120 @@
1
+ /** Connect durable planning tickets to DSH's owned jobs and managed processes. */
2
+ import { createRequire } from 'node:module'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+ import type { Context } from '@deepseek-ai/cordis'
6
+ import type { Agent } from '@deepseek-ai/dsh-agent'
7
+ import type { JobOutcome } from '@deepseek-ai/dsh-jobs'
8
+ import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
9
+ import { settleTurnHandoffTicket, writeTicketJson, type TurnHandoffTicket } from './turn-deadline.ts'
10
+
11
+ declare module '@deepseek-ai/dsh-jobs' {
12
+ interface JobKindMap { 'gotry-planning': 'gotry-planning' }
13
+ }
14
+
15
+ const require = createRequire(import.meta.url)
16
+
17
+ export async function startTurnHandoffJob(
18
+ ctx: Context, ticket: TurnHandoffTicket, owner: Agent, stateRoot: string,
19
+ ): Promise<TurnHandoffTicket> {
20
+ if (ticket.status !== 'open') return ticket
21
+ const jobs = ctx.get('jobs')
22
+ const subprocess = ctx.get('subprocess')
23
+ if (!jobs || !subprocess) return ticket // Queued, never announced as running.
24
+ let current = ticket
25
+ let handle: SubprocessHandle | undefined
26
+ let cancelled = false
27
+ let release!: (ok: boolean) => void
28
+ const prepared = new Promise<boolean>(resolve => { release = resolve })
29
+ let announceStarted!: () => void
30
+ const started = new Promise<void>(resolve => { announceStarted = resolve })
31
+ const fail = async (reason: string) => {
32
+ await settleTurnHandoffTicket(stateRoot, current, 'failed', `# Planning failed\n\n${reason}\n\nNo completed plan was produced.`, reason)
33
+ current = { ...current, status: 'failed', error: reason }
34
+ }
35
+ try {
36
+ const jobId = jobs.start({
37
+ kind: 'gotry-planning', owner: owner.id, label: `${ticket.id}: ${ticket.objective.slice(0, 100)}`, outputLimitBytes: 12_000,
38
+ run() {
39
+ return {
40
+ cancel() { cancelled = true; handle?.terminate() },
41
+ done: (async (): Promise<JobOutcome> => {
42
+ if (!await prepared) { announceStarted(); return { status: 'failed', detail: 'ticket persistence failed' } }
43
+ let timer: ReturnType<typeof setTimeout> | undefined
44
+ try {
45
+ if (cancelled) throw new Error('planning cancelled')
46
+ const extension = import.meta.url.endsWith('.ts') ? 'ts' : 'js'
47
+ const collector = join(import.meta.dirname, '..', 'scripts', `turn-handoff-collect.${extension}`)
48
+ const argv = extension === 'ts'
49
+ ? [process.execPath, require.resolve('tsx/cli'), collector, ticket.id, stateRoot]
50
+ : [process.execPath, collector, ticket.id, stateRoot]
51
+ handle = subprocess.spawn({
52
+ argv, cwd: ticket.workspaceCwd ?? stateRoot,
53
+ stdio: { stdin: 'ignore', stdout: { maxBytes: 16_000 }, stderr: { maxBytes: 16_000 } },
54
+ // Collector cleanup includes the wrapper's detached DSH group
55
+ // (5s+1s), then its own 8s+1s bound and durable settlement.
56
+ graceMs: 11_000,
57
+ // This dedicated planning child inherits the configured model
58
+ // environment explicitly; secrets never enter tickets or output.
59
+ env: { GOTRY_HANDOFF_JOB_ID: current.jobId,
60
+ GOTRY_HANDOFF_PLANNER_BIN: process.env.GOTRY_HANDOFF_PLANNER_BIN,
61
+ GOTRY_HANDOFF_PLANNER_TIMEOUT_MS: process.env.GOTRY_HANDOFF_PLANNER_TIMEOUT_MS,
62
+ DSH_HOME: process.env.DSH_HOME,
63
+ LLM_API_KEY: process.env.LLM_API_KEY, LLM_BASE_URL: process.env.LLM_BASE_URL,
64
+ LLM_MODEL: process.env.LLM_MODEL, GOTRY_LLM_MODEL: process.env.GOTRY_LLM_MODEL,
65
+ DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL: process.env.DEEPSEEK_BASE_URL },
66
+ })
67
+ announceStarted()
68
+ const plannerTimeout = Number(process.env.GOTRY_HANDOFF_PLANNER_TIMEOUT_MS)
69
+ const timeout = Number.isSafeInteger(plannerTimeout) && plannerTimeout > 0 ? plannerTimeout : 900_000
70
+ timer = setTimeout(() => handle!.terminate(), timeout + 12_000)
71
+ const direct = await handle.done.then(
72
+ outcome => ({ ok: true as const, outcome }),
73
+ error => ({ ok: false as const, error }),
74
+ )
75
+ // Direct success or failure does not prove managed-range exit.
76
+ if (!direct.ok) handle.terminate()
77
+ try { await handle.waitForExit() }
78
+ catch (cleanupError) {
79
+ handle.terminate()
80
+ const cleanupDiagnostic = cleanupError instanceof Error ? cleanupError.message : 'unknown cleanup error'
81
+ const cleanupReason = `managed-range cleanup could not be confirmed: ${cleanupDiagnostic}`
82
+ if (!direct.ok) {
83
+ const diagnostic = direct.error instanceof Error ? direct.error.message : 'planning failed'
84
+ throw new AggregateError([direct.error, cleanupError], `${diagnostic}; ${cleanupReason}`)
85
+ }
86
+ throw new Error(cleanupReason, { cause: cleanupError })
87
+ }
88
+ if (!direct.ok) throw direct.error
89
+ const outcome = direct.outcome
90
+ const path = join(stateRoot, 'gotry-state', 'turn-handoffs', `${ticket.id}.json`)
91
+ const settled = JSON.parse(await readFile(path, 'utf8')) as TurnHandoffTicket
92
+ if (cancelled) throw new Error('planning cancelled')
93
+ if (outcome.exitCode !== 0 || settled.status !== 'settled' || settled.deliverableFile !== `${ticket.id}.deliverable.md`) {
94
+ throw new Error(settled.error ?? `planning collector failed (exit ${outcome.exitCode ?? outcome.signal})`)
95
+ }
96
+ const deliverablePath = join(stateRoot, 'gotry-state', 'turn-handoffs', settled.deliverableFile)
97
+ const content = await readFile(deliverablePath, 'utf8')
98
+ if (!content.trim()) throw new Error('planning collector produced an empty deliverable')
99
+ current = settled
100
+ return { status: 'completed', detail: `ticket ${ticket.id} settled`, result: `Planning completed: ${ticket.id}. Deliverable: ${deliverablePath}\nCall present with this exact path, then report the result.\n\n${content.slice(0, 8_000)}` }
101
+ } catch (error) {
102
+ const reason = error instanceof Error ? error.message : 'planning failed'
103
+ await fail(reason)
104
+ return { status: cancelled ? 'killed' : 'failed', detail: reason, result: `Ticket ${ticket.id} failed: ${reason}` }
105
+ } finally { if (timer) clearTimeout(timer); announceStarted() }
106
+ })(),
107
+ }
108
+ },
109
+ })
110
+ current = { ...ticket, status: 'running', jobId }
111
+ await writeTicketJson(stateRoot, current)
112
+ release(true)
113
+ await started
114
+ return current
115
+ } catch (error) {
116
+ release(false)
117
+ await fail(error instanceof Error ? error.message : 'background job registration failed')
118
+ return current
119
+ }
120
+ }