@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.
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +9 -9
- package/bin/gotry-inner.js +7 -1
- package/cordis.gotry-patch.yml +2 -0
- package/data/yunnan-pack.json +30 -27
- package/dist/capabilities/artifacts.js +41 -4
- package/dist/capabilities/channel-registry.js +4 -4
- package/dist/capabilities/doctor.js +3 -3
- package/dist/capabilities/effect.js +1 -1
- package/dist/capabilities/session/extension-bridge.js +3 -3
- package/dist/capabilities/session-search.js +1 -1
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +2 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +135 -2
- package/dist/scripts/bootstrap-tests.js +11 -6
- package/dist/scripts/doctor-tests.js +2 -2
- package/dist/scripts/gotry-backend-kernel-tests.js +1 -1
- package/dist/scripts/issue-271-liveness-tests.js +6 -0
- package/dist/scripts/persona-sim-tests.js +19 -1
- package/dist/scripts/persona-sim.js +1 -1
- package/dist/scripts/session-dida-live-e2e.js +1 -1
- package/dist/scripts/turn-handoff-collect-tests.js +7 -1
- package/dist/scripts/turn-handoff-collect.js +68 -20
- package/dist/scripts/turn-handoff-job-tests.js +222 -0
- package/dist/scripts/turn-handoff-web-e2e.js +240 -0
- package/dist/scripts/unified-tests.js +71 -30
- package/dist/src/backend/modules/session-search.js +1 -1
- package/dist/src/index.js +30 -14
- package/dist/src/turn-deadline.js +113 -16
- package/dist/src/turn-handoff-job.js +124 -0
- package/extension/README.md +1 -1
- package/extension/README.zh-CN.md +1 -1
- package/extension/background.js +1 -1
- package/extension/content-bridge.js +1 -1
- package/extension/content-main.js +1 -1
- package/extension/manifest.json +2 -2
- package/package.json +2 -1
- package/ts/capabilities/artifacts.ts +40 -5
- package/ts/capabilities/session/extension-bridge.ts +3 -3
- package/ts/capabilities/session-search.ts +1 -1
- package/ts/scripts/turn-handoff-collect.ts +63 -21
- package/ts/src/index.ts +30 -14
- package/ts/src/turn-deadline.ts +111 -18
- 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
|
+
}
|