mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
fix: give direct chats the current request, delivery contract, and full task
Commit9198bf6made direct chats send only the latest message text and rely on the native session. After1c9aabba rebuilt session received the continuation text instead, and that text deliberately excludes the latest user message, so the Agent never saw the request it was answering. Direct prompts also lost the deliverable contract (.meldwork-output/), the read-only media warning, staged attachment paths, and regeneration and recovery instructions, which is why generated files were not captured and three media tests failed onbb55fad. - New directPromptFor() leads with the user's own words. A new or rebuilt native session gets the earlier conversation plus "Current request:"; a resumed session gets the request alone. The full delivery and language contract is sent once per native session, a one-line reminder afterwards. Staged inputs, recovery and regeneration instructions are appended again. - The shared deliveryContract() is used by group and direct prompts. - Isolated (task-graph) invocations again neither resume nor persist the conversation's native session (3ce8ee4made them resume it, breaking the "approved prompt only" contract). - The current task is no longer cut to 6,000 characters, and longer messages no longer fail every turn with LOCAL_RUN_REQUIRED_CONTEXT_OVERFLOW: submitted messages are already capped at 20,000 characters, so the full request is delivered. Checks: local-workspace-harness (73) and local-workspace-context-packs (15) pass on this tree, plus the direct, media and custom-label tests in local-workspace and native-streaming (18), five of which failed onbb55fad.
This commit is contained in:
1 parent
2da40db63d
commit
624a099bdd
5 files changed
+120
-29
No files matched your search
@@ -17,6 +17,7 @@ const {
|
||||
normalizeContextPackId,
|
||||
} = require('../collaboration/context-pack-records.cjs')
|
||||
const {
|
||||
defaultAgentLabel,
|
||||
HERMES_WORKSPACE_ACP_ENABLED,
|
||||
MAX_MESSAGE_ATTACHMENTS,
|
||||
abortableOperation,
|
||||
@@ -29,6 +30,7 @@ const {
|
||||
settleWithin,
|
||||
} = require('./local-workspace-inputs.cjs')
|
||||
const { assertLocalSkillExecution } = require('../skills/local-skill-contract.cjs')
|
||||
const { directPromptFor } = require('./local-workspace-context.cjs')
|
||||
const { outboundWirePayloadBytes } = require('../collaboration/outbound-payload.cjs')
|
||||
const { processRunScheduler } = require('../runs/run-scheduler.cjs')
|
||||
const {
|
||||
@@ -515,6 +517,9 @@ class LocalWorkspaceAgentInvocation {
|
||||
this.scheduleRunCheckpoint = options.scheduleRunCheckpoint
|
||||
this.emitChanged = options.emitChanged
|
||||
this.promptFor = options.promptFor
|
||||
this.agentLabel = typeof options.agentLabel === 'function'
|
||||
? options.agentLabel
|
||||
: kind => defaultAgentLabel(kind)
|
||||
this.persistSessionState = options.persistSessionState
|
||||
this.createAttemptContextPack = options.createAttemptContextPack
|
||||
this.recordContextDelivery = options.recordContextDelivery
|
||||
@@ -704,7 +709,9 @@ class LocalWorkspaceAgentInvocation {
|
||||
}
|
||||
: null
|
||||
const idempotencyMode = agent.idempotencyMode === 'durable' ? 'durable' : 'none'
|
||||
const resolvedSession = reviewOnly
|
||||
// Isolated invocations must see only their approved prompt, so they never
|
||||
// resume or persist the conversation's native session.
|
||||
const resolvedSession = reviewOnly || isolated
|
||||
? {
|
||||
key: this.sessionKey(group.id, kind),
|
||||
sessionRef: '',
|
||||
@@ -1278,10 +1285,24 @@ class LocalWorkspaceAgentInvocation {
|
||||
const buildPrompt = (afterKind, contextPackage) => isolated || frozen
|
||||
? v4Prompt
|
||||
: group.conversationType === 'direct'
|
||||
? (sessionRotated
|
||||
? (contextPackage.continuationText || contextPackage.stableText
|
||||
|| contextPackage.currentTaskText)
|
||||
: contextPackage.currentTaskText)
|
||||
? [
|
||||
// A resumed native session already holds the conversation. A new
|
||||
// or rebuilt session needs the earlier turns and, always, the
|
||||
// current request plus the per-turn delivery contract.
|
||||
directPromptFor({
|
||||
group,
|
||||
label: this.agentLabel(kind),
|
||||
packed: contextPackage,
|
||||
skillHints: context.skillHints || [],
|
||||
knowledgeBaseHints: context.knowledgeBaseHints || [],
|
||||
includeHistory: sessionRotated || !sessionRef,
|
||||
}),
|
||||
stagedAgentInputPrompt(stagedInputs),
|
||||
runtimeInstruction ? `Harness recovery task:\n${runtimeInstruction}` : '',
|
||||
regenerationInstruction
|
||||
? `Fresh response instruction:\n${regenerationInstruction}`
|
||||
: '',
|
||||
].filter(Boolean).join('\n\n')
|
||||
: [
|
||||
context.v4 === true && v4Prompt
|
||||
? v4Prompt
|
||||
@@ -1441,7 +1462,7 @@ class LocalWorkspaceAgentInvocation {
|
||||
onSessionRef: (nextSessionRef, metadata = {}) => {
|
||||
if (agentCallbacksClosed || agentController.signal.aborted) return
|
||||
noteWatchdogProgress()
|
||||
if (reviewOnly) return
|
||||
if (reviewOnly || isolated) return
|
||||
const transport = ['legacy', 'acp'].includes(metadata?.transport)
|
||||
? metadata.transport
|
||||
: ''
|
||||
@@ -1454,7 +1475,7 @@ class LocalWorkspaceAgentInvocation {
|
||||
}))
|
||||
},
|
||||
onSessionInvalidated: () => {
|
||||
if (kind !== 'hermes' || reviewOnly || resumedPermission) return null
|
||||
if (kind !== 'hermes' || reviewOnly || isolated || resumedPermission) return null
|
||||
return { prompt: rebuildFreshSession() }
|
||||
},
|
||||
signal: agentController.signal,
|
||||
@@ -1904,7 +1925,7 @@ class LocalWorkspaceAgentInvocation {
|
||||
pendingMessage.metadata,
|
||||
)
|
||||
: null
|
||||
if (!reviewOnly) {
|
||||
if (!reviewOnly && !isolated) {
|
||||
this.persistSessionState(key, result.sessionRef || sessionRef, completedSessionMeta(
|
||||
sessionMeta, sessionProvenance, taskId, {
|
||||
promptChars: prompt.length,
|
||||
|
||||
@@ -21,10 +21,15 @@ const {
|
||||
normalizeSkillHint,
|
||||
skillHintsPrompt,
|
||||
} = require('./local-workspace-inputs.cjs')
|
||||
const { MAX_MESSAGE_TEXT_CHARS } = require('./local-workspace-contracts.cjs')
|
||||
|
||||
const CONTINUATION_STABLE_CONTEXT_TEXT_LIMIT = 1400
|
||||
const CONTINUATION_RECENT_CONTEXT_TEXT_LIMIT = 4600
|
||||
const CURRENT_TASK_TEXT_LIMIT = 6000
|
||||
// User messages are already capped at MAX_MESSAGE_TEXT_CHARS when they are
|
||||
// submitted. The current task must reach the Agent in full: truncating it, or
|
||||
// failing the turn when it is longer than a fixed budget, made long pasted
|
||||
// documents unusable. The extra room covers the attached-file note.
|
||||
const CURRENT_TASK_TEXT_LIMIT = MAX_MESSAGE_TEXT_CHARS + 2000
|
||||
const V4_SNAPSHOT_HISTORY_LIMIT = 16
|
||||
const NATURAL_TRANSCRIPT_TEXT_LIMIT = 48000
|
||||
const NATURAL_TRANSCRIPT_ENTRY_LIMIT = 20000
|
||||
@@ -808,6 +813,72 @@ function packedPromptContext({
|
||||
}
|
||||
}
|
||||
|
||||
// The write-access and deliverable contract applies to every turn, including
|
||||
// direct chats that rely on the native Agent session for history. Without it
|
||||
// Agents do not know where to put files, so generated outputs are never
|
||||
// captured and read-only chats claim media that does not exist.
|
||||
function deliveryContract(group) {
|
||||
return group?.allowWrite
|
||||
? [
|
||||
'Workspace access: You may create and edit files in the conversation working directory. When the user requests a deliverable, execute the work instead of returning only a plan.',
|
||||
'Deliverable capture contract: Save or copy each final document, code file, diff, structured-data file, media file, or bundle into .meldwork-output/ in the conversation working directory.',
|
||||
'Only files created or changed during this run are captured as durable outputs. Do not claim a deliverable exists until the real file has been written there.',
|
||||
'When the user asks for an image, audio, or video, do not claim it was generated unless a real file exists.',
|
||||
'Supported chat attachments include common images, media, documents, source code, configuration text, and archives. Other recognized outputs remain durable Artifacts.',
|
||||
].join('\n')
|
||||
: [
|
||||
'This conversation is read-only. Do not claim that a media file was generated or delivered because no writable output can be attached.',
|
||||
'If the user requests generated media, explain that workspace write access must be enabled before a real file can be delivered.',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
// Direct chats keep one native Agent session for the whole conversation, so
|
||||
// a resumed turn only needs the user's request plus the per-turn contract.
|
||||
// When the session is new or was rebuilt, the earlier conversation is packed
|
||||
// before the request so the Agent never loses either of them.
|
||||
function directPromptFor({
|
||||
group,
|
||||
label = '',
|
||||
packed,
|
||||
skillHints = [],
|
||||
knowledgeBaseHints = [],
|
||||
includeHistory = false,
|
||||
}) {
|
||||
const currentTaskText = packed?.currentTaskText || '(none)'
|
||||
const latestId = String(packed?.latestUserMessageId || '')
|
||||
const hasPriorContext = includeHistory
|
||||
&& (packed?.continuationSourceMessageIds || []).some(id => id && id !== latestId)
|
||||
// The full contract is delivered once per native session; resumed turns
|
||||
// carry a one-line reminder so long chats do not re-send the same block.
|
||||
const notes = includeHistory
|
||||
? [
|
||||
label ? `You are working in this Meldwork chat as ${label}.` : '',
|
||||
deliveryContract(group),
|
||||
responseLanguagePrompt(packed?.latestUserLanguage),
|
||||
]
|
||||
: [
|
||||
group?.allowWrite
|
||||
? 'Save deliverables into .meldwork-output/ in the working directory; do not claim a file exists until it was written.'
|
||||
: 'This chat is read-only; do not claim that a file or media was generated or delivered.',
|
||||
'Answer in the language of the current request unless it asks for another language.',
|
||||
]
|
||||
notes.push(skillHintsPrompt(skillHints), knowledgeBaseHintsPrompt(knowledgeBaseHints))
|
||||
return [
|
||||
...(hasPriorContext
|
||||
? [
|
||||
'Earlier conversation in this chat (reference only; verify it before relying on it):',
|
||||
packed.continuationText,
|
||||
'',
|
||||
'Current request:',
|
||||
]
|
||||
: []),
|
||||
currentTaskText,
|
||||
'',
|
||||
'Meldwork conversation notes:',
|
||||
...notes.filter(Boolean),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function promptFor({
|
||||
group,
|
||||
kind,
|
||||
@@ -837,18 +908,7 @@ function promptFor({
|
||||
packed?.currentTaskText || '(none)',
|
||||
'Treat older group messages and conclusions as reference only. Do not continue an older task unless the current user task explicitly asks you to do so.',
|
||||
].join('\n')
|
||||
const mediaDelivery = group.allowWrite
|
||||
? [
|
||||
'Workspace access: You may create and edit files in the conversation working directory. When the user requests a deliverable, execute the work instead of returning only a plan.',
|
||||
'Deliverable capture contract: Save or copy each final document, code file, diff, structured-data file, media file, or bundle into .meldwork-output/ in the conversation working directory.',
|
||||
'Only files created or changed during this run are captured as durable outputs. Do not claim a deliverable exists until the real file has been written there.',
|
||||
'When the user asks for an image, audio, or video, do not claim it was generated unless a real file exists.',
|
||||
'Supported chat attachments include common images, media, documents, source code, configuration text, and archives. Other recognized outputs remain durable Artifacts.',
|
||||
].join('\n')
|
||||
: [
|
||||
'This conversation is read-only. Do not claim that a media file was generated or delivered because no writable output can be attached.',
|
||||
'If the user requests generated media, explain that workspace write access must be enabled before a real file can be delivered.',
|
||||
].join('\n')
|
||||
const mediaDelivery = deliveryContract(group)
|
||||
if (promptMode === 'continuation') {
|
||||
const continuationMediaDelivery = group.allowWrite
|
||||
? 'Workspace write access remains enabled for this task; execute requested deliverables and capture them as required.'
|
||||
@@ -898,6 +958,8 @@ function promptFor({
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deliveryContract,
|
||||
directPromptFor,
|
||||
packNaturalDiscussionTranscript,
|
||||
clearSessionState,
|
||||
openClawSessionRef,
|
||||
|
||||
@@ -267,6 +267,7 @@ class LocalWorkspace extends EventEmitter {
|
||||
})
|
||||
this.agentInvocation = new LocalWorkspaceAgentInvocation({
|
||||
state: () => this.state,
|
||||
agentLabel: kind => this.agentLabel(kind),
|
||||
detectedAgents: () => this.detectedAgents,
|
||||
activeRuns: this.activeRuns,
|
||||
runAgentTimeoutMs: this.runAgentTimeoutMs,
|
||||
|
||||
@@ -463,24 +463,25 @@ test('long in-budget requests verify the exact prompt that is actually delivered
|
||||
assert.equal(workspace.contextPacks.compareDelivery(delivery.deliveryRecordId).status, 'match')
|
||||
})
|
||||
|
||||
test('required current-task overflow fails closed before Agent execution', async (t) => {
|
||||
test('a long current task reaches the Agent in full instead of failing the turn', async (t) => {
|
||||
const { directory, calls, options } = fixture()
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
const workspace = new LocalWorkspace(options)
|
||||
await workspace.refreshAgents()
|
||||
const group = workspace.createGroup({
|
||||
name: 'Required context overflow', agentKinds: ['codex'], workdir: directory,
|
||||
name: 'Long current task', agentKinds: ['codex'], workdir: directory,
|
||||
conversationType: 'direct', directAgentKind: 'codex',
|
||||
})
|
||||
const request = `Generate this image exactly: ${'dense visual direction '.repeat(500)}`
|
||||
// Longer than the former 6,000-character budget that failed every turn.
|
||||
const request = `Generate this image exactly: ${'dense visual direction '.repeat(800)}end-of-request`
|
||||
|
||||
await workspace.sendMessage({ groupId: group.id, text: request })
|
||||
|
||||
assert.equal(calls.length, 0)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].prompt.includes(request.trim()), true)
|
||||
assert.equal(workspace.snapshot().messages.some(message => (
|
||||
message.system?.key === 'system.agentCallFailed'
|
||||
&& message.system.params.reason === 'LOCAL_RUN_REQUIRED_CONTEXT_OVERFLOW'
|
||||
)), true)
|
||||
message.system?.params?.reason === 'LOCAL_RUN_REQUIRED_CONTEXT_OVERFLOW'
|
||||
)), false)
|
||||
})
|
||||
|
||||
test('Codex-shaped stdin delivery verifies the exact budgeted prompt', async (t) => {
|
||||
|
||||
@@ -1377,7 +1377,13 @@ test('direct conversations force manual mode and reuse their group Agent session
|
||||
assert.equal(direct.directAgentKind, 'codex')
|
||||
assert.deepEqual(calls.map(call => call.agent.kind), ['codex', 'codex'])
|
||||
assert.deepEqual(calls.map(call => call.runOptions.sessionRef), ['', 'codex-session'])
|
||||
assert.deepEqual(calls.map(call => call.prompt), ['第一条', '第二条'])
|
||||
// Direct chats lead with the user's own words and carry no group framing.
|
||||
// The delivery contract is sent in full when the native session starts and
|
||||
// as a one-line reminder on resumed turns.
|
||||
assert.deepEqual(calls.map(call => call.prompt.split('\n')[0]), ['第一条', '第二条'])
|
||||
assert.equal(calls.some(call => /You are participating|group Session|Group topic/.test(call.prompt)), false)
|
||||
assert.match(calls[0].prompt, /\.meldwork-output\//)
|
||||
assert.ok(calls[1].prompt.length < calls[0].prompt.length)
|
||||
assert.equal(calls.some(call => call.prompt.includes('MELDWORK_CONSENSUS')), false)
|
||||
assert.equal(workspace.snapshot().messages.some(message => message.threadRootId), false)
|
||||
const restored = new LocalWorkspace(options)
|
||||
|
||||
Reference in new issue
Block a user